/*
 * Copyright 2017 - 2025 the original author or authors.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see [https://www.gnu.org/licenses/]
 */
package infra.bytecode;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;

/**
 * A parser to make a {@link ClassVisitor} visit a ClassFile structure, as defined in the Java
 * Virtual Machine Specification (JVMS). This class parses the ClassFile content and calls the
 * appropriate visit methods of a given {@link ClassVisitor} for each field, method and bytecode
 * instruction encountered.
 *
 * @author Eric Bruneton
 * @author Eugene Kuleshov
 * @author <a href="https://github.com/TAKETODAY">Harry Yang</a>
 * @see <a href="https://docs.oracle.com/javase/specs/jvms/se9/html/jvms-4.html">JVMS 4</a>
 */
public class ClassReader {

  /**
   * A flag to skip the Code attributes. If this flag is set the Code attributes are neither parsed
   * nor visited.
   */
  public static final int SKIP_CODE = 1;

  /**
   * A flag to skip the SourceFile, SourceDebugExtension, LocalVariableTable,
   * LocalVariableTypeTable, LineNumberTable and MethodParameters attributes. If this flag is set
   * these attributes are neither parsed nor visited (i.e. {@link ClassVisitor#visitSource}, {@link
   * MethodVisitor#visitLocalVariable}, {@link MethodVisitor#visitLineNumber} and {@link
   * MethodVisitor#visitParameter} are not called).
   */
  public static final int SKIP_DEBUG = 2;

  /**
   * A flag to skip the StackMap and StackMapTable attributes. If this flag is set these attributes
   * are neither parsed nor visited (i.e. {@link MethodVisitor#visitFrame} is not called). This flag
   * is useful when the {@link ClassWriter#COMPUTE_FRAMES} option is used: it avoids visiting frames
   * that will be ignored and recomputed from scratch.
   */
  public static final int SKIP_FRAMES = 4;

  /**
   * A flag to expand the stack map frames. By default stack map frames are visited in their
   * original format (i.e. "expanded" for classes whose version is less than V1_6, and "compressed"
   * for the other classes). If this flag is set, stack map frames are always visited in expanded
   * format (this option adds a decompression/compression step in ClassReader and ClassWriter which
   * degrades performance quite a lot).
   */
  public static final int EXPAND_FRAMES = 8;

  /**
   * A flag to expand the ASM specific instructions into an equivalent sequence of standard bytecode
   * instructions. When resolving a forward jump it may happen that the signed 2 bytes offset
   * reserved for it is not sufficient to store the bytecode offset. In this case the jump
   * instruction is replaced with a temporary ASM specific instruction using an unsigned 2 bytes
   * offset (see {@link Label#resolve}). This internal flag is used to re-read classes containing
   * such instructions, in order to replace them with standard instructions. In addition, when this
   * flag is used, goto_w and jsr_w are <i>not</i> converted into goto and jsr, to make sure that
   * infinite loops where a goto_w is replaced with a goto in ClassReader and converted back to a
   * goto_w in ClassWriter cannot occur.
   */
  static final int EXPAND_ASM_INSNS = 256;

  /** The maximum size of array to allocate. */
  private static final int MAX_BUFFER_SIZE = 1024 * 1024;

  /** The size of the temporary byte array used to read class input streams chunk by chunk. */
  private static final int INPUT_STREAM_DATA_CHUNK_SIZE = 4096;

  /** The offset in bytes of the ClassFile's access_flags field. */
  public final int header;

  /**
   * A byte array containing the JVMS ClassFile structure to be parsed. <i>The content of this array
   * must not be modified. This field is intended for {@link Attribute} sub classes, and is normally
   * not needed by class visitors.</i>
   *
   * <p>NOTE: the ClassFile structure can start at any offset within this array, i.e. it does not
   * necessarily start at offset 0. Use {@link #getItem} and {@link #header} to get correct
   * ClassFile element offsets within this byte array.
   */
  final byte[] classFileBuffer;

  /**
   * The offset in bytes, in {@link #classFileBuffer}, of each cp_info entry of the ClassFile's
   * constant_pool array, <i>plus one</i>. In other words, the offset of constant pool entry i is
   * given by cpInfoOffsets[i] - 1, i.e. its cp_info's tag field is given by b[cpInfoOffsets[i] -
   * 1].
   */
  private final int[] cpInfoOffsets;

  /**
   * The String objects corresponding to the CONSTANT_Utf8 constant pool items. This cache avoids
   * multiple parsing of a given CONSTANT_Utf8 constant pool item.
   */
  private final String[] constantUtf8Values;

  /**
   * The ConstantDynamic objects corresponding to the CONSTANT_Dynamic constant pool items. This
   * cache avoids multiple parsing of a given CONSTANT_Dynamic constant pool item.
   */
  private final ConstantDynamic[] constantDynamicValues;

  /**
   * The start offsets in {@link #classFileBuffer} of each element of the bootstrap_methods array
   * (in the BootstrapMethods attribute).
   *
   * @see <a href="https://docs.oracle.com/javase/specs/jvms/se9/html/jvms-4.html#jvms-4.7.23">JVMS
   * 4.7.23</a>
   */
  private final int[] bootstrapMethodOffsets;

  /**
   * A conservative estimate of the maximum length of the strings contained in the constant pool of
   * the class.
   */
  private final int maxStringLength;

  // -----------------------------------------------------------------------------------------------
  // Constructors
  // -----------------------------------------------------------------------------------------------

  /**
   * Constructs a new {@link ClassReader} object.
   *
   * @param classFile the JVMS ClassFile structure to be read.
   */
  public ClassReader(final byte[] classFile) {
    this(classFile, true);
  }

  /**
   * Constructs a new {@link ClassReader} object.
   *
   * @param classFile the JVMS ClassFile structure to be read.
   */
  public ClassReader(final byte[] classFile, final boolean checkClassVersion) {
    this(classFile, 0, checkClassVersion);
  }

  /**
   * Constructs a new {@link ClassReader} object.
   *
   * @param classFileBuffer a byte array containing the JVMS ClassFile structure to be read.
   * @param classFileOffset the offset in byteBuffer of the first byte of the ClassFile to be read.
   */
  public ClassReader(final byte[] classFileBuffer, final int classFileOffset) {
    this(classFileBuffer, classFileOffset, /* checkClassVersion = */ true);
  }

  /**
   * Constructs a new {@link ClassReader} object. <i>This internal constructor must not be exposed
   * as a public API</i>.
   *
   * @param classFileBuffer a byte array containing the JVMS ClassFile structure to be read.
   * @param classFileOffset the offset in byteBuffer of the first byte of the ClassFile to be read.
   * @param checkClassVersion whether to check the class version or not.
   */
  ClassReader(final byte[] classFileBuffer, final int classFileOffset, final boolean checkClassVersion) {
    this.classFileBuffer = classFileBuffer;
    // Check the class' major_version. This field is after the magic and minor_version fields, which
    // use 4 and 2 bytes respectively.
    if (checkClassVersion && readShort(classFileOffset + 6) > Opcodes.V26) {
      throw new IllegalArgumentException(
              "Unsupported class file major version " + readShort(classFileOffset + 6));
    }
    // Create the constant pool arrays. The constant_pool_count field is after the magic,
    // minor_version and major_version fields, which use 4, 2 and 2 bytes respectively.
    int constantPoolCount = readUnsignedShort(classFileOffset + 8);
    int[] cpInfoOffsets = new int[constantPoolCount];
    this.cpInfoOffsets = cpInfoOffsets;
    this.constantUtf8Values = new String[constantPoolCount];
    // Compute the offset of each constant pool entry, as well as a conservative estimate of the
    // maximum length of the constant pool strings. The first constant pool entry is after the
    // magic, minor_version, major_version and constant_pool_count fields, which use 4, 2, 2 and 2
    // bytes respectively.
    int currentCpInfoIndex = 1;
    int currentCpInfoOffset = classFileOffset + 10;
    int currentMaxStringLength = 0;
    boolean hasBootstrapMethods = false;
    boolean hasConstantDynamic = false;
    // The offset of the other entries depend on the total size of all the previous entries.
    while (currentCpInfoIndex < constantPoolCount) {
      cpInfoOffsets[currentCpInfoIndex++] = currentCpInfoOffset + 1;
      int cpInfoSize;
      switch (classFileBuffer[currentCpInfoOffset]) {
        case Symbol.CONSTANT_FIELDREF_TAG, Symbol.CONSTANT_METHODREF_TAG,
             Symbol.CONSTANT_INTERFACE_METHODREF_TAG, Symbol.CONSTANT_INTEGER_TAG,
             Symbol.CONSTANT_FLOAT_TAG, Symbol.CONSTANT_NAME_AND_TYPE_TAG -> cpInfoSize = 5;
        case Symbol.CONSTANT_DYNAMIC_TAG -> {
          cpInfoSize = 5;
          hasBootstrapMethods = true;
          hasConstantDynamic = true;
        }
        case Symbol.CONSTANT_INVOKE_DYNAMIC_TAG -> {
          cpInfoSize = 5;
          hasBootstrapMethods = true;
        }
        case Symbol.CONSTANT_LONG_TAG, Symbol.CONSTANT_DOUBLE_TAG -> {
          cpInfoSize = 9;
          currentCpInfoIndex++;
        }
        case Symbol.CONSTANT_UTF8_TAG -> {
          cpInfoSize = 3 + readUnsignedShort(currentCpInfoOffset + 1);
          if (cpInfoSize > currentMaxStringLength) {
            // The size in bytes of this CONSTANT_Utf8 structure provides a conservative estimate
            // of the length in characters of the corresponding string, and is much cheaper to
            // compute than this exact length.
            currentMaxStringLength = cpInfoSize;
          }
        }
        case Symbol.CONSTANT_METHOD_HANDLE_TAG -> cpInfoSize = 4;
        case Symbol.CONSTANT_CLASS_TAG, Symbol.CONSTANT_STRING_TAG,
             Symbol.CONSTANT_METHOD_TYPE_TAG, Symbol.CONSTANT_PACKAGE_TAG,
             Symbol.CONSTANT_MODULE_TAG -> cpInfoSize = 3;
        default -> throw new IllegalArgumentException();
      }
      currentCpInfoOffset += cpInfoSize;
    }
    maxStringLength = currentMaxStringLength;
    // The Classfile's access_flags field is just after the last constant pool entry.
    header = currentCpInfoOffset;

    // Allocate the cache of ConstantDynamic values, if there is at least one.
    constantDynamicValues = hasConstantDynamic ? new ConstantDynamic[constantPoolCount] : null;
    // Read the BootstrapMethods attribute, if any (only get the offset of each method).
    bootstrapMethodOffsets = hasBootstrapMethods ? readBootstrapMethodsAttribute(currentMaxStringLength) : null;
  }

  /**
   * Constructs a new {@link ClassReader} object.
   *
   * @param inputStream an input stream of the JVMS ClassFile structure to be read. This input
   * stream must contain nothing more than the ClassFile structure itself. It is read from its
   * current position to its end.
   * @throws IOException if a problem occurs during reading.
   */
  public ClassReader(final InputStream inputStream) throws IOException {
    this(readStream(inputStream, false));
  }

  /**
   * Constructs a new {@link ClassReader} object.
   *
   * @param className the fully qualified name of the class to be read. The ClassFile structure is
   * retrieved with the current class loader's {@link ClassLoader#getSystemResourceAsStream}.
   * @throws IOException if an exception occurs during reading.
   */
  public ClassReader(final String className) throws IOException {
    this(readStream(
            ClassLoader.getSystemResourceAsStream(className.replace('.', '/') + ".class"), true));
  }

  /**
   * Reads the given input stream and returns its content as a byte array.
   *
   * @param inputStream an input stream.
   * @param close true to close the input stream after reading.
   * @return the content of the given input stream.
   * @throws IOException if a problem occurs during reading.
   */
  private static byte[] readStream(final InputStream inputStream, final boolean close) throws IOException {
    if (inputStream == null) {
      throw new IOException("Class not found");
    }
    int bufferSize = calculateBufferSize(inputStream);
    try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
      byte[] data = new byte[bufferSize];
      int bytesRead;
      int readCount = 0;
      while ((bytesRead = inputStream.read(data, 0, bufferSize)) != -1) {
        outputStream.write(data, 0, bytesRead);
        readCount++;
      }
      outputStream.flush();
      if (readCount == 1) {
        return data;
      }
      return outputStream.toByteArray();
    }
    finally {
      if (close) {
        inputStream.close();
      }
    }
  }

  private static int calculateBufferSize(final InputStream inputStream) throws IOException {
    int expectedLength = inputStream.available();
    /*
     * Some implementations can return 0 while holding available data
     * (e.g. new FileInputStream("/proc/a_file"))
     * Also in some pathological cases a very small number might be returned,
     * and in this case we use default size
     */
    if (expectedLength < 256) {
      return INPUT_STREAM_DATA_CHUNK_SIZE;
    }
    return Math.min(expectedLength, MAX_BUFFER_SIZE);
  }

  // -----------------------------------------------------------------------------------------------
  // Accessors
  // -----------------------------------------------------------------------------------------------

  /**
   * Returns the class's access flags (see {@link Opcodes}). This value may not reflect Deprecated
   * and Synthetic flags when bytecode is before 1.5 and those flags are represented by attributes.
   *
   * @return the class access flags.
   * @see ClassVisitor#visit(int, int, String, String, String, String[])
   */
  public int getAccess() {
    return readUnsignedShort(header);
  }

  /**
   * Returns the internal name of the class (see {@link Type#getInternalName()}).
   *
   * @return the internal class name.
   * @see ClassVisitor#visit(int, int, String, String, String, String[])
   */
  public String getClassName() {
    // this_class is just after the access_flags field (using 2 bytes).
    return readClass(header + 2, new char[maxStringLength]);
  }

  /**
   * Returns the internal of name of the super class (see {@link Type#getInternalName()}). For
   * interfaces, the super class is {@link Object}.
   *
   * @return the internal name of the super class, or {@literal null} for {@link Object} class.
   * @see ClassVisitor#visit(int, int, String, String, String, String[])
   */
  public String getSuperName() {
    // super_class is after the access_flags and this_class fields (2 bytes each).
    return readClass(header + 4, new char[maxStringLength]);
  }

  /**
   * Returns the internal names of the implemented interfaces (see {@link Type#getInternalName()}).
   *
   * @return the internal names of the directly implemented interfaces. Inherited implemented
   * interfaces are not returned.
   * @see ClassVisitor#visit(int, int, String, String, String, String[])
   */
  public String[] getInterfaces() {
    // interfaces_count is after the access_flags, this_class and super_class fields (2 bytes each).
    int currentOffset = header + 6;
    int interfacesCount = readUnsignedShort(currentOffset);
    String[] interfaces = new String[interfacesCount];
    if (interfacesCount > 0) {
      char[] charBuffer = new char[maxStringLength];
      for (int i = 0; i < interfacesCount; ++i) {
        currentOffset += 2;
        interfaces[i] = readClass(currentOffset, charBuffer);
      }
    }
    return interfaces;
  }

  // -----------------------------------------------------------------------------------------------
  // Public methods
  // -----------------------------------------------------------------------------------------------

  /**
   * Makes the given visitor visit the JVMS ClassFile structure passed to the constructor of this
   * {@link ClassReader}.
   *
   * @param classVisitor the visitor that must visit this class.
   * @param parsingOptions the options to use to parse this class. One or more of {@link
   * #SKIP_CODE}, {@link #SKIP_DEBUG}, {@link #SKIP_FRAMES} or {@link #EXPAND_FRAMES}.
   */
  public void accept(final ClassVisitor classVisitor, final int parsingOptions) {
    accept(classVisitor, new Attribute[0], parsingOptions);
  }

  /**
   * Makes the given visitor visit the JVMS ClassFile structure passed to the constructor of this
   * {@link ClassReader}.
   *
   * @param classVisitor the visitor that must visit this class.
   * @param attributePrototypes prototypes of the attributes that must be parsed during the visit of
   * the class. Any attribute whose type is not equal to the type of one the prototypes will not
   * be parsed: its byte array value will be passed unchanged to the ClassWriter. <i>This may
   * corrupt it if this value contains references to the constant pool, or has syntactic or
   * semantic links with a class element that has been transformed by a class adapter between
   * the reader and the writer</i>.
   * @param parsingOptions the options to use to parse this class. One or more of {@link
   * #SKIP_CODE}, {@link #SKIP_DEBUG}, {@link #SKIP_FRAMES} or {@link #EXPAND_FRAMES}.
   */
  public void accept(final ClassVisitor classVisitor, final Attribute[] attributePrototypes, final int parsingOptions) {
    Context context = new Context();
    context.attributePrototypes = attributePrototypes;
    context.parsingOptions = parsingOptions;
    // Read the access_flags, this_class, super_class, interface_count and interfaces fields.
    char[] charBuffer = new char[maxStringLength];
    context.charBuffer = charBuffer;
    int currentOffset = header;
    int accessFlags = readUnsignedShort(currentOffset);
    String thisClass = readClass(currentOffset + 2, charBuffer);
    String superClass = readClass(currentOffset + 4, charBuffer);
    String[] interfaces = new String[readUnsignedShort(currentOffset + 6)];
    currentOffset += 8;
    for (int i = 0; i < interfaces.length; ++i) {
      interfaces[i] = readClass(currentOffset, charBuffer);
      currentOffset += 2;
    }

    // Read the class attributes (the variables are ordered as in Section 4.7 of the JVMS).
    // Attribute offsets exclude the attribute_name_index and attribute_length fields.
    // - The offset of the InnerClasses attribute, or 0.
    int innerClassesOffset = 0;
    // - The offset of the EnclosingMethod attribute, or 0.
    int enclosingMethodOffset = 0;
    // - The string corresponding to the Signature attribute, or null.
    String signature = null;
    // - The string corresponding to the SourceFile attribute, or null.
    String sourceFile = null;
    // - The string corresponding to the SourceDebugExtension attribute, or null.
    String sourceDebugExtension = null;
    // - The offset of the RuntimeVisibleAnnotations attribute, or 0.
    int runtimeVisibleAnnotationsOffset = 0;
    // - The offset of the RuntimeInvisibleAnnotations attribute, or 0.
    int runtimeInvisibleAnnotationsOffset = 0;
    // - The offset of the RuntimeVisibleTypeAnnotations attribute, or 0.
    int runtimeVisibleTypeAnnotationsOffset = 0;
    // - The offset of the RuntimeInvisibleTypeAnnotations attribute, or 0.
    int runtimeInvisibleTypeAnnotationsOffset = 0;
    // - The offset of the Module attribute, or 0.
    int moduleOffset = 0;
    // - The offset of the ModulePackages attribute, or 0.
    int modulePackagesOffset = 0;
    // - The string corresponding to the ModuleMainClass attribute, or null.
    String moduleMainClass = null;
    // - The string corresponding to the NestHost attribute, or null.
    String nestHostClass = null;
    // - The offset of the NestMembers attribute, or 0.
    int nestMembersOffset = 0;
    // - The offset of the PermittedSubclasses attribute, or 0
    int permittedSubclassesOffset = 0;
    // - The offset of the Record attribute, or 0.
    int recordOffset = 0;
    // - The non standard attributes (linked with their {@link Attribute#nextAttribute} field).
    //   This list in the <i>reverse order</i> or their order in the ClassFile structure.
    Attribute attributes = null;

    int currentAttributeOffset = getFirstAttributeOffset();
    for (int i = readUnsignedShort(currentAttributeOffset - 2); i > 0; --i) {
      // Read the attribute_info's attribute_name and attribute_length fields.
      String attributeName = readUTF8(currentAttributeOffset, charBuffer);
      int attributeLength = readInt(currentAttributeOffset + 2);
      currentAttributeOffset += 6;
      // The tests are sorted in decreasing frequency order (based on frequencies observed on
      // typical classes).
      if (Constants.SOURCE_FILE.equals(attributeName)) {
        sourceFile = readUTF8(currentAttributeOffset, charBuffer);
      }
      else if (Constants.INNER_CLASSES.equals(attributeName)) {
        innerClassesOffset = currentAttributeOffset;
      }
      else if (Constants.ENCLOSING_METHOD.equals(attributeName)) {
        enclosingMethodOffset = currentAttributeOffset;
      }
      else if (Constants.NEST_HOST.equals(attributeName)) {
        nestHostClass = readClass(currentAttributeOffset, charBuffer);
      }
      else if (Constants.NEST_MEMBERS.equals(attributeName)) {
        nestMembersOffset = currentAttributeOffset;
      }
      else if (Constants.PERMITTED_SUBCLASSES.equals(attributeName)) {
        permittedSubclassesOffset = currentAttributeOffset;
      }
      else if (Constants.SIGNATURE.equals(attributeName)) {
        signature = readUTF8(currentAttributeOffset, charBuffer);
      }
      else if (Constants.RUNTIME_VISIBLE_ANNOTATIONS.equals(attributeName)) {
        runtimeVisibleAnnotationsOffset = currentAttributeOffset;
      }
      else if (Constants.RUNTIME_VISIBLE_TYPE_ANNOTATIONS.equals(attributeName)) {
        runtimeVisibleTypeAnnotationsOffset = currentAttributeOffset;
      }
      else if (Constants.DEPRECATED.equals(attributeName)) {
        accessFlags |= Opcodes.ACC_DEPRECATED;
      }
      else if (Constants.SYNTHETIC.equals(attributeName)) {
        accessFlags |= Opcodes.ACC_SYNTHETIC;
      }
      else if (Constants.SOURCE_DEBUG_EXTENSION.equals(attributeName)) {
        if (attributeLength > classFileBuffer.length - currentAttributeOffset) {
          throw new IllegalArgumentException();
        }
        sourceDebugExtension = readUtf(currentAttributeOffset, attributeLength, new char[attributeLength]);
      }
      else if (Constants.RUNTIME_INVISIBLE_ANNOTATIONS.equals(attributeName)) {
        runtimeInvisibleAnnotationsOffset = currentAttributeOffset;
      }
      else if (Constants.RUNTIME_INVISIBLE_TYPE_ANNOTATIONS.equals(attributeName)) {
        runtimeInvisibleTypeAnnotationsOffset = currentAttributeOffset;
      }
      else if (Constants.RECORD.equals(attributeName)) {
        recordOffset = currentAttributeOffset;
        accessFlags |= Opcodes.ACC_RECORD;
      }
      else if (Constants.MODULE.equals(attributeName)) {
        moduleOffset = currentAttributeOffset;
      }
      else if (Constants.MODULE_MAIN_CLASS.equals(attributeName)) {
        moduleMainClass = readClass(currentAttributeOffset, charBuffer);
      }
      else if (Constants.MODULE_PACKAGES.equals(attributeName)) {
        modulePackagesOffset = currentAttributeOffset;
      }
      else if (!Constants.BOOTSTRAP_METHODS.equals(attributeName)) {
        // The BootstrapMethods attribute is read in the constructor.
        Attribute attribute = readAttribute(attributePrototypes, attributeName,
                currentAttributeOffset, attributeLength, charBuffer, -1, null);
        attribute.nextAttribute = attributes;
        attributes = attribute;
      }
      currentAttributeOffset += attributeLength;
    }

    // Visit the class declaration. The minor_version and major_version fields start 6 bytes before
    // the first constant pool entry, which itself starts at cpInfoOffsets[1] - 1 (by definition).
    classVisitor.visit(
            readInt(cpInfoOffsets[1] - 7), accessFlags, thisClass, signature, superClass, interfaces);

    // Visit the SourceFile and SourceDebugExtenstion attributes.
    if ((parsingOptions & SKIP_DEBUG) == 0
            && (sourceFile != null || sourceDebugExtension != null)) {
      classVisitor.visitSource(sourceFile, sourceDebugExtension);
    }

    // Visit the Module, ModulePackages and ModuleMainClass attributes.
    if (moduleOffset != 0) {
      readModuleAttributes(classVisitor, context, moduleOffset, modulePackagesOffset, moduleMainClass);
    }

    // Visit the NestHost attribute.
    if (nestHostClass != null) {
      classVisitor.visitNestHost(nestHostClass);
    }

    // Visit the EnclosingMethod attribute.
    if (enclosingMethodOffset != 0) {
      String className = readClass(enclosingMethodOffset, charBuffer);
      int methodIndex = readUnsignedShort(enclosingMethodOffset + 2);
      String name = methodIndex == 0 ? null : readUTF8(cpInfoOffsets[methodIndex], charBuffer);
      String type = methodIndex == 0 ? null : readUTF8(cpInfoOffsets[methodIndex] + 2, charBuffer);
      classVisitor.visitOuterClass(className, name, type);
    }

    // Visit the RuntimeVisibleAnnotations attribute.
    if (runtimeVisibleAnnotationsOffset != 0) {
      int numAnnotations = readUnsignedShort(runtimeVisibleAnnotationsOffset);
      int currentAnnotationOffset = runtimeVisibleAnnotationsOffset + 2;
      while (numAnnotations-- > 0) {
        // Parse the type_index field.
        String annotationDescriptor = readUTF8(currentAnnotationOffset, charBuffer);
        currentAnnotationOffset += 2;
        // Parse num_element_value_pairs and element_value_pairs and visit these values.
        currentAnnotationOffset = readElementValues(
                classVisitor.visitAnnotation(annotationDescriptor, /* visible = */ true),
                currentAnnotationOffset, /* named = */ true, charBuffer);
      }
    }

    // Visit the RuntimeInvisibleAnnotations attribute.
    if (runtimeInvisibleAnnotationsOffset != 0) {
      int numAnnotations = readUnsignedShort(runtimeInvisibleAnnotationsOffset);
      int currentAnnotationOffset = runtimeInvisibleAnnotationsOffset + 2;
      while (numAnnotations-- > 0) {
        // Parse the type_index field.
        String annotationDescriptor = readUTF8(currentAnnotationOffset, charBuffer);
        currentAnnotationOffset += 2;
        // Parse num_element_value_pairs and element_value_pairs and visit these values.
        currentAnnotationOffset = readElementValues(
                classVisitor.visitAnnotation(annotationDescriptor, /* visible = */ false),
                currentAnnotationOffset, /* named = */ true, charBuffer);
      }
    }

    // Visit the RuntimeVisibleTypeAnnotations attribute.
    if (runtimeVisibleTypeAnnotationsOffset != 0) {
      int numAnnotations = readUnsignedShort(runtimeVisibleTypeAnnotationsOffset);
      int currentAnnotationOffset = runtimeVisibleTypeAnnotationsOffset + 2;
      while (numAnnotations-- > 0) {
        // Parse the target_type, target_info and target_path fields.
        currentAnnotationOffset = readTypeAnnotationTarget(context, currentAnnotationOffset);
        // Parse the type_index field.
        String annotationDescriptor = readUTF8(currentAnnotationOffset, charBuffer);
        currentAnnotationOffset += 2;
        // Parse num_element_value_pairs and element_value_pairs and visit these values.
        currentAnnotationOffset = readElementValues(
                classVisitor.visitTypeAnnotation(context.currentTypeAnnotationTarget,
                        context.currentTypeAnnotationTargetPath, annotationDescriptor, /* visible = */ true),
                currentAnnotationOffset, /* named = */ true, charBuffer);
      }
    }

    // Visit the RuntimeInvisibleTypeAnnotations attribute.
    if (runtimeInvisibleTypeAnnotationsOffset != 0) {
      int numAnnotations = readUnsignedShort(runtimeInvisibleTypeAnnotationsOffset);
      int currentAnnotationOffset = runtimeInvisibleTypeAnnotationsOffset + 2;
      while (numAnnotations-- > 0) {
        // Parse the target_type, target_info and target_path fields.
        currentAnnotationOffset = readTypeAnnotationTarget(context, currentAnnotationOffset);
        // Parse the type_index field.
        String annotationDescriptor = readUTF8(currentAnnotationOffset, charBuffer);
        currentAnnotationOffset += 2;
        // Parse num_element_value_pairs and element_value_pairs and visit these values.
        currentAnnotationOffset = readElementValues(classVisitor.visitTypeAnnotation(context.currentTypeAnnotationTarget,
                        context.currentTypeAnnotationTargetPath, annotationDescriptor, /* visible = */ false),
                currentAnnotationOffset, /* named = */ true, charBuffer);
      }
    }

    // Visit the non standard attributes.
    while (attributes != null) {
      // Copy and reset the nextAttribute field so that it can also be used in ClassWriter.
      Attribute nextAttribute = attributes.nextAttribute;
      attributes.nextAttribute = null;
      classVisitor.visitAttribute(attributes);
      attributes = nextAttribute;
    }

    // Visit the NestedMembers attribute.
    if (nestMembersOffset != 0) {
      int numberOfNestMembers = readUnsignedShort(nestMembersOffset);
      int currentNestMemberOffset = nestMembersOffset + 2;
      while (numberOfNestMembers-- > 0) {
        classVisitor.visitNestMember(readClass(currentNestMemberOffset, charBuffer));
        currentNestMemberOffset += 2;
      }
    }

    // Visit the PermittedSubclasses attribute.
    if (permittedSubclassesOffset != 0) {
      int numberOfPermittedSubclasses = readUnsignedShort(permittedSubclassesOffset);
      int currentPermittedSubclassesOffset = permittedSubclassesOffset + 2;
      while (numberOfPermittedSubclasses-- > 0) {
        classVisitor.visitPermittedSubclass(
                readClass(currentPermittedSubclassesOffset, charBuffer));
        currentPermittedSubclassesOffset += 2;
      }
    }

    // Visit the InnerClasses attribute.
    if (innerClassesOffset != 0) {
      int numberOfClasses = readUnsignedShort(innerClassesOffset);
      int currentClassesOffset = innerClassesOffset + 2;
      while (numberOfClasses-- > 0) {
        classVisitor.visitInnerClass(
                readClass(currentClassesOffset, charBuffer),
                readClass(currentClassesOffset + 2, charBuffer),
                readUTF8(currentClassesOffset + 4, charBuffer),
                readUnsignedShort(currentClassesOffset + 6));
        currentClassesOffset += 8;
      }
    }

    // Visit Record components.
    if (recordOffset != 0) {
      int recordComponentsCount = readUnsignedShort(recordOffset);
      recordOffset += 2;
      while (recordComponentsCount-- > 0) {
        recordOffset = readRecordComponent(classVisitor, context, recordOffset);
      }
    }

    // Visit the fields and methods.
    int fieldsCount = readUnsignedShort(currentOffset);
    currentOffset += 2;
    while (fieldsCount-- > 0) {
      currentOffset = readField(classVisitor, context, currentOffset);
    }
    int methodsCount = readUnsignedShort(currentOffset);
    currentOffset += 2;
    while (methodsCount-- > 0) {
      currentOffset = readMethod(classVisitor, context, currentOffset);
    }

    // Visit the end of the class.
    classVisitor.visitEnd();
  }

  // ----------------------------------------------------------------------------------------------
  // Methods to parse modules, fields and methods
  // ----------------------------------------------------------------------------------------------

  /**
   * Reads the Module, ModulePackages and ModuleMainClass attributes and visit them.
   *
   * @param classVisitor the current class visitor
   * @param context information about the class being parsed.
   * @param moduleOffset the offset of the Module attribute (excluding the attribute_info's
   * attribute_name_index and attribute_length fields).
   * @param modulePackagesOffset the offset of the ModulePackages attribute (excluding the
   * attribute_info's attribute_name_index and attribute_length fields), or 0.
   * @param moduleMainClass the string corresponding to the ModuleMainClass attribute, or {@literal
   * null}.
   */
  private void readModuleAttributes(final ClassVisitor classVisitor, final Context context,
          final int moduleOffset, final int modulePackagesOffset, final String moduleMainClass) {
    char[] buffer = context.charBuffer;

    // Read the module_name_index, module_flags and module_version_index fields and visit them.
    int currentOffset = moduleOffset;
    String moduleName = readModule(currentOffset, buffer);
    int moduleFlags = readUnsignedShort(currentOffset + 2);
    String moduleVersion = readUTF8(currentOffset + 4, buffer);
    currentOffset += 6;
    ModuleVisitor moduleVisitor = classVisitor.visitModule(moduleName, moduleFlags, moduleVersion);
    if (moduleVisitor == null) {
      return;
    }

    // Visit the ModuleMainClass attribute.
    if (moduleMainClass != null) {
      moduleVisitor.visitMainClass(moduleMainClass);
    }

    // Visit the ModulePackages attribute.
    if (modulePackagesOffset != 0) {
      int packageCount = readUnsignedShort(modulePackagesOffset);
      int currentPackageOffset = modulePackagesOffset + 2;
      while (packageCount-- > 0) {
        moduleVisitor.visitPackage(readPackage(currentPackageOffset, buffer));
        currentPackageOffset += 2;
      }
    }

    // Read the 'requires_count' and 'requires' fields.
    int requiresCount = readUnsignedShort(currentOffset);
    currentOffset += 2;
    while (requiresCount-- > 0) {
      // Read the requires_index, requires_flags and requires_version fields and visit them.
      String requires = readModule(currentOffset, buffer);
      int requiresFlags = readUnsignedShort(currentOffset + 2);
      String requiresVersion = readUTF8(currentOffset + 4, buffer);
      currentOffset += 6;
      moduleVisitor.visitRequire(requires, requiresFlags, requiresVersion);
    }

    // Read the 'exports_count' and 'exports' fields.
    int exportsCount = readUnsignedShort(currentOffset);
    currentOffset += 2;
    while (exportsCount-- > 0) {
      // Read the exports_index, exports_flags, exports_to_count and exports_to_index fields
      // and visit them.
      String exports = readPackage(currentOffset, buffer);
      int exportsFlags = readUnsignedShort(currentOffset + 2);
      int exportsToCount = readUnsignedShort(currentOffset + 4);
      currentOffset += 6;
      String[] exportsTo = null;
      if (exportsToCount != 0) {
        exportsTo = new String[exportsToCount];
        for (int i = 0; i < exportsToCount; ++i) {
          exportsTo[i] = readModule(currentOffset, buffer);
          currentOffset += 2;
        }
      }
      moduleVisitor.visitExport(exports, exportsFlags, exportsTo);
    }

    // Reads the 'opens_count' and 'opens' fields.
    int opensCount = readUnsignedShort(currentOffset);
    currentOffset += 2;
    while (opensCount-- > 0) {
      // Read the opens_index, opens_flags, opens_to_count and opens_to_index fields and visit them.
      String opens = readPackage(currentOffset, buffer);
      int opensFlags = readUnsignedShort(currentOffset + 2);
      int opensToCount = readUnsignedShort(currentOffset + 4);
      currentOffset += 6;
      String[] opensTo = null;
      if (opensToCount != 0) {
        opensTo = new String[opensToCount];
        for (int i = 0; i < opensToCount; ++i) {
          opensTo[i] = readModule(currentOffset, buffer);
          currentOffset += 2;
        }
      }
      moduleVisitor.visitOpen(opens, opensFlags, opensTo);
    }

    // Read the 'uses_count' and 'uses' fields.
    int usesCount = readUnsignedShort(currentOffset);
    currentOffset += 2;
    while (usesCount-- > 0) {
      moduleVisitor.visitUse(readClass(currentOffset, buffer));
      currentOffset += 2;
    }

    // Read the  'provides_count' and 'provides' fields.
    int providesCount = readUnsignedShort(currentOffset);
    currentOffset += 2;
    while (providesCount-- > 0) {
      // Read the provides_index, provides_with_count and provides_with_index fields and visit them.
      String provides = readClass(currentOffset, buffer);
      int providesWithCount = readUnsignedShort(currentOffset + 2);
      currentOffset += 4;
      String[] providesWith = new String[providesWithCount];
      for (int i = 0; i < providesWithCount; ++i) {
        providesWith[i] = readClass(currentOffset, buffer);
        currentOffset += 2;
      }
      moduleVisitor.visitProvide(provides, providesWith);
    }

    // Visit the end of the module attributes.
    moduleVisitor.visitEnd();
  }

  /**
   * Reads a record component and visit it.
   *
   * @param classVisitor the current class visitor
   * @param context information about the class being parsed.
   * @param recordComponentOffset the offset of the current record component.
   * @return the offset of the first byte following the record component.
   */
  private int readRecordComponent(final ClassVisitor classVisitor, final Context context, final int recordComponentOffset) {
    char[] charBuffer = context.charBuffer;

    int currentOffset = recordComponentOffset;
    String name = readUTF8(currentOffset, charBuffer);
    String descriptor = readUTF8(currentOffset + 2, charBuffer);
    currentOffset += 4;

    // Read the record component attributes (the variables are ordered as in Section 4.7 of the
    // JVMS).

    // Attribute offsets exclude the attribute_name_index and attribute_length fields.
    // - The string corresponding to the Signature attribute, or null.
    String signature = null;
    // - The offset of the RuntimeVisibleAnnotations attribute, or 0.
    int runtimeVisibleAnnotationsOffset = 0;
    // - The offset of the RuntimeInvisibleAnnotations attribute, or 0.
    int runtimeInvisibleAnnotationsOffset = 0;
    // - The offset of the RuntimeVisibleTypeAnnotations attribute, or 0.
    int runtimeVisibleTypeAnnotationsOffset = 0;
    // - The offset of the RuntimeInvisibleTypeAnnotations attribute, or 0.
    int runtimeInvisibleTypeAnnotationsOffset = 0;
    // - The non standard attributes (linked with their {@link Attribute#nextAttribute} field).
    //   This list in the <i>reverse order</i> or their order in the ClassFile structure.
    Attribute attributes = null;

    int attributesCount = readUnsignedShort(currentOffset);
    currentOffset += 2;
    while (attributesCount-- > 0) {
      // Read the attribute_info's attribute_name and attribute_length fields.
      String attributeName = readUTF8(currentOffset, charBuffer);
      int attributeLength = readInt(currentOffset + 2);
      currentOffset += 6;
      // The tests are sorted in decreasing frequency order (based on frequencies observed on
      // typical classes).
      if (Constants.SIGNATURE.equals(attributeName)) {
        signature = readUTF8(currentOffset, charBuffer);
      }
      else if (Constants.RUNTIME_VISIBLE_ANNOTATIONS.equals(attributeName)) {
        runtimeVisibleAnnotationsOffset = currentOffset;
      }
      else if (Constants.RUNTIME_VISIBLE_TYPE_ANNOTATIONS.equals(attributeName)) {
        runtimeVisibleTypeAnnotationsOffset = currentOffset;
      }
      else if (Constants.RUNTIME_INVISIBLE_ANNOTATIONS.equals(attributeName)) {
        runtimeInvisibleAnnotationsOffset = currentOffset;
      }
      else if (Constants.RUNTIME_INVISIBLE_TYPE_ANNOTATIONS.equals(attributeName)) {
        runtimeInvisibleTypeAnnotationsOffset = currentOffset;
      }
      else {
        Attribute attribute = readAttribute(context.attributePrototypes, attributeName,
                currentOffset, attributeLength, charBuffer, -1, null);
        attribute.nextAttribute = attributes;
        attributes = attribute;
      }
      currentOffset += attributeLength;
    }

    RecordComponentVisitor recordComponentVisitor = classVisitor.visitRecordComponent(name, descriptor, signature);
    if (recordComponentVisitor == null) {
      return currentOffset;
    }

    // Visit the RuntimeVisibleAnnotations attribute.
    if (runtimeVisibleAnnotationsOffset != 0) {
      int numAnnotations = readUnsignedShort(runtimeVisibleAnnotationsOffset);
      int currentAnnotationOffset = runtimeVisibleAnnotationsOffset + 2;
      while (numAnnotations-- > 0) {
        // Parse the type_index field.
        String annotationDescriptor = readUTF8(currentAnnotationOffset, charBuffer);
        currentAnnotationOffset += 2;
        // Parse num_element_value_pairs and element_value_pairs and visit these values.
        currentAnnotationOffset = readElementValues(
                recordComponentVisitor.visitAnnotation(annotationDescriptor, /* visible = */ true),
                currentAnnotationOffset, /* named = */ true, charBuffer);
      }
    }

    // Visit the RuntimeInvisibleAnnotations attribute.
    if (runtimeInvisibleAnnotationsOffset != 0) {
      int numAnnotations = readUnsignedShort(runtimeInvisibleAnnotationsOffset);
      int currentAnnotationOffset = runtimeInvisibleAnnotationsOffset + 2;
      while (numAnnotations-- > 0) {
        // Parse the type_index field.
        String annotationDescriptor = readUTF8(currentAnnotationOffset, charBuffer);
        currentAnnotationOffset += 2;
        // Parse num_element_value_pairs and element_value_pairs and visit these values.
        currentAnnotationOffset = readElementValues(
                recordComponentVisitor.visitAnnotation(annotationDescriptor, /* visible = */ false),
                currentAnnotationOffset, /* named = */ true, charBuffer);
      }
    }

    // Visit the RuntimeVisibleTypeAnnotations attribute.
    if (runtimeVisibleTypeAnnotationsOffset != 0) {
      int numAnnotations = readUnsignedShort(runtimeVisibleTypeAnnotationsOffset);
      int currentAnnotationOffset = runtimeVisibleTypeAnnotationsOffset + 2;
      while (numAnnotations-- > 0) {
        // Parse the target_type, target_info and target_path fields.
        currentAnnotationOffset = readTypeAnnotationTarget(context, currentAnnotationOffset);
        // Parse the type_index field.
        String annotationDescriptor = readUTF8(currentAnnotationOffset, charBuffer);
        currentAnnotationOffset += 2;
        // Parse num_element_value_pairs and element_value_pairs and visit these values.
        currentAnnotationOffset = readElementValues(
                recordComponentVisitor.visitTypeAnnotation(context.currentTypeAnnotationTarget,
                        context.currentTypeAnnotationTargetPath, annotationDescriptor, /* visible = */ true),
                currentAnnotationOffset, /* named = */ true, charBuffer);
      }
    }

    // Visit the RuntimeInvisibleTypeAnnotations attribute.
    if (runtimeInvisibleTypeAnnotationsOffset != 0) {
      int numAnnotations = readUnsignedShort(runtimeInvisibleTypeAnnotationsOffset);
      int currentAnnotationOffset = runtimeInvisibleTypeAnnotationsOffset + 2;
      while (numAnnotations-- > 0) {
        // Parse the target_type, target_info and target_path fields.
        currentAnnotationOffset = readTypeAnnotationTarget(context, currentAnnotationOffset);
        // Parse the type_index field.
        String annotationDescriptor = readUTF8(currentAnnotationOffset, charBuffer);
        currentAnnotationOffset += 2;
        // Parse num_element_value_pairs and element_value_pairs and visit these values.
        currentAnnotationOffset = readElementValues(
                recordComponentVisitor.visitTypeAnnotation(context.currentTypeAnnotationTarget,
                        context.currentTypeAnnotationTargetPath, annotationDescriptor, /* visible = */ false),
                currentAnnotationOffset, /* named = */ true, charBuffer);
      }
    }

    // Visit the non standard attributes.
    while (attributes != null) {
      // Copy and reset the nextAttribute field so that it can also be used in FieldWriter.
      Attribute nextAttribute = attributes.nextAttribute;
      attributes.nextAttribute = null;
      recordComponentVisitor.visitAttribute(attributes);
      attributes = nextAttribute;
    }

    // Visit the end of the field.
    recordComponentVisitor.visitEnd();
    return currentOffset;
  }

  /**
   * Reads a JVMS field_info structure and makes the given visitor visit it.
   *
   * @param classVisitor the visitor that must visit the field.
   * @param context information about the class being parsed.
   * @param fieldInfoOffset the start offset of the field_info structure.
   * @return the offset of the first byte following the field_info structure.
   */
  private int readField(final ClassVisitor classVisitor, final Context context, final int fieldInfoOffset) {
    char[] charBuffer = context.charBuffer;

    // Read the access_flags, name_index and descriptor_index fields.
    int currentOffset = fieldInfoOffset;
    int accessFlags = readUnsignedShort(currentOffset);
    String name = readUTF8(currentOffset + 2, charBuffer);
    String descriptor = readUTF8(currentOffset + 4, charBuffer);
    currentOffset += 6;

    // Read the field attributes (the variables are ordered as in Section 4.7 of the JVMS).
    // Attribute offsets exclude the attribute_name_index and attribute_length fields.
    // - The value corresponding to the ConstantValue attribute, or null.
    Object constantValue = null;
    // - The string corresponding to the Signature attribute, or null.
    String signature = null;
    // - The offset of the RuntimeVisibleAnnotations attribute, or 0.
    int runtimeVisibleAnnotationsOffset = 0;
    // - The offset of the RuntimeInvisibleAnnotations attribute, or 0.
    int runtimeInvisibleAnnotationsOffset = 0;
    // - The offset of the RuntimeVisibleTypeAnnotations attribute, or 0.
    int runtimeVisibleTypeAnnotationsOffset = 0;
    // - The offset of the RuntimeInvisibleTypeAnnotations attribute, or 0.
    int runtimeInvisibleTypeAnnotationsOffset = 0;
    // - The non standard attributes (linked with their {@link Attribute#nextAttribute} field).
    //   This list in the <i>reverse order</i> or their order in the ClassFile structure.
    Attribute attributes = null;

    int attributesCount = readUnsignedShort(currentOffset);
    currentOffset += 2;
    while (attributesCount-- > 0) {
      // Read the attribute_info's attribute_name and attribute_length fields.
      String attributeName = readUTF8(currentOffset, charBuffer);
      int attributeLength = readInt(currentOffset + 2);
      currentOffset += 6;
      // The tests are sorted in decreasing frequency order (based on frequencies observed on
      // typical classes).
      if (Constants.CONSTANT_VALUE.equals(attributeName)) {
        int constantvalueIndex = readUnsignedShort(currentOffset);
        constantValue = constantvalueIndex == 0 ? null : readConst(constantvalueIndex, charBuffer);
      }
      else if (Constants.SIGNATURE.equals(attributeName)) {
        signature = readUTF8(currentOffset, charBuffer);
      }
      else if (Constants.DEPRECATED.equals(attributeName)) {
        accessFlags |= Opcodes.ACC_DEPRECATED;
      }
      else if (Constants.SYNTHETIC.equals(attributeName)) {
        accessFlags |= Opcodes.ACC_SYNTHETIC;
      }
      else if (Constants.RUNTIME_VISIBLE_ANNOTATIONS.equals(attributeName)) {
        runtimeVisibleAnnotationsOffset = currentOffset;
      }
      else if (Constants.RUNTIME_VISIBLE_TYPE_ANNOTATIONS.equals(attributeName)) {
        runtimeVisibleTypeAnnotationsOffset = currentOffset;
      }
      else if (Constants.RUNTIME_INVISIBLE_ANNOTATIONS.equals(attributeName)) {
        runtimeInvisibleAnnotationsOffset = currentOffset;
      }
      else if (Constants.RUNTIME_INVISIBLE_TYPE_ANNOTATIONS.equals(attributeName)) {
        runtimeInvisibleTypeAnnotationsOffset = currentOffset;
      }
      else {
        Attribute attribute =
                readAttribute(
                        context.attributePrototypes,
                        attributeName,
                        currentOffset,
                        attributeLength,
                        charBuffer,
                        -1,
                        null);
        attribute.nextAttribute = attributes;
        attributes = attribute;
      }
      currentOffset += attributeLength;
    }

    // Visit the field declaration.
    FieldVisitor fieldVisitor =
            classVisitor.visitField(accessFlags, name, descriptor, signature, constantValue);
    if (fieldVisitor == null) {
      return currentOffset;
    }

    // Visit the RuntimeVisibleAnnotations attribute.
    if (runtimeVisibleAnnotationsOffset != 0) {
      int numAnnotations = readUnsignedShort(runtimeVisibleAnnotationsOffset);
      int currentAnnotationOffset = runtimeVisibleAnnotationsOffset + 2;
      while (numAnnotations-- > 0) {
        // Parse the type_index field.
        String annotationDescriptor = readUTF8(currentAnnotationOffset, charBuffer);
        currentAnnotationOffset += 2;
        // Parse num_element_value_pairs and element_value_pairs and visit these values.
        currentAnnotationOffset =
                readElementValues(
                        fieldVisitor.visitAnnotation(annotationDescriptor, /* visible = */ true),
                        currentAnnotationOffset,
                        /* named = */ true,
                        charBuffer);
      }
    }

    // Visit the RuntimeInvisibleAnnotations attribute.
    if (runtimeInvisibleAnnotationsOffset != 0) {
      int numAnnotations = readUnsignedShort(runtimeInvisibleAnnotationsOffset);
      int currentAnnotationOffset = runtimeInvisibleAnnotationsOffset + 2;
      while (numAnnotations-- > 0) {
        // Parse the type_index field.
        String annotationDescriptor = readUTF8(currentAnnotationOffset, charBuffer);
        currentAnnotationOffset += 2;
        // Parse num_element_value_pairs and element_value_pairs and visit these values.
        currentAnnotationOffset =
                readElementValues(
                        fieldVisitor.visitAnnotation(annotationDescriptor, /* visible = */ false),
                        currentAnnotationOffset,
                        /* named = */ true,
                        charBuffer);
      }
    }

    // Visit the RuntimeVisibleTypeAnnotations attribute.
    if (runtimeVisibleTypeAnnotationsOffset != 0) {
      int numAnnotations = readUnsignedShort(runtimeVisibleTypeAnnotationsOffset);
      int currentAnnotationOffset = runtimeVisibleTypeAnnotationsOffset + 2;
      while (numAnnotations-- > 0) {
        // Parse the target_type, target_info and target_path fields.
        currentAnnotationOffset = readTypeAnnotationTarget(context, currentAnnotationOffset);
        // Parse the type_index field.
        String annotationDescriptor = readUTF8(currentAnnotationOffset, charBuffer);
        currentAnnotationOffset += 2;
        // Parse num_element_value_pairs and element_value_pairs and visit these values.
        currentAnnotationOffset =
                readElementValues(
                        fieldVisitor.visitTypeAnnotation(
                                context.currentTypeAnnotationTarget,
                                context.currentTypeAnnotationTargetPath,
                                annotationDescriptor,
                                /* visible = */ true),
                        currentAnnotationOffset,
                        /* named = */ true,
                        charBuffer);
      }
    }

    // Visit the RuntimeInvisibleTypeAnnotations attribute.
    if (runtimeInvisibleTypeAnnotationsOffset != 0) {
      int numAnnotations = readUnsignedShort(runtimeInvisibleTypeAnnotationsOffset);
      int currentAnnotationOffset = runtimeInvisibleTypeAnnotationsOffset + 2;
      while (numAnnotations-- > 0) {
        // Parse the target_type, target_info and target_path fields.
        currentAnnotationOffset = readTypeAnnotationTarget(context, currentAnnotationOffset);
        // Parse the type_index field.
        String annotationDescriptor = readUTF8(currentAnnotationOffset, charBuffer);
        currentAnnotationOffset += 2;
        // Parse num_element_value_pairs and element_value_pairs and visit these values.
        currentAnnotationOffset =
                readElementValues(
                        fieldVisitor.visitTypeAnnotation(
                                context.currentTypeAnnotationTarget,
                                context.currentTypeAnnotationTargetPath,
                                annotationDescriptor,
                                /* visible = */ false),
                        currentAnnotationOffset,
                        /* named = */ true,
                        charBuffer);
      }
    }

    // Visit the non standard attributes.
    while (attributes != null) {
      // Copy and reset the nextAttribute field so that it can also be used in FieldWriter.
      Attribute nextAttribute = attributes.nextAttribute;
      attributes.nextAttribute = null;
      fieldVisitor.visitAttribute(attributes);
      attributes = nextAttribute;
    }

    // Visit the end of the field.
    fieldVisitor.visitEnd();
    return currentOffset;
  }

  /**
   * Reads a JVMS method_info structure and makes the given visitor visit it.
   *
   * @param classVisitor the visitor that must visit the method.
   * @param context information about the class being parsed.
   * @param methodInfoOffset the start offset of the method_info structure.
   * @return the offset of the first byte following the method_info structure.
   */
  private int readMethod(final ClassVisitor classVisitor, final Context context, final int methodInfoOffset) {
    char[] charBuffer = context.charBuffer;

    // Read the access_flags, name_index and descriptor_index fields.
    int currentOffset = methodInfoOffset;
    context.currentMethodAccessFlags = readUnsignedShort(currentOffset);
    context.currentMethodName = readUTF8(currentOffset + 2, charBuffer);
    context.currentMethodDescriptor = readUTF8(currentOffset + 4, charBuffer);
    currentOffset += 6;

    // Read the method attributes (the variables are ordered as in Section 4.7 of the JVMS).
    // Attribute offsets exclude the attribute_name_index and attribute_length fields.
    // - The offset of the Code attribute, or 0.
    int codeOffset = 0;
    // - The offset of the Exceptions attribute, or 0.
    int exceptionsOffset = 0;
    // - The strings corresponding to the Exceptions attribute, or null.
    String[] exceptions = null;
    // - Whether the method has a Synthetic attribute.
    boolean synthetic = false;
    // - The constant pool index contained in the Signature attribute, or 0.
    int signatureIndex = 0;
    // - The offset of the RuntimeVisibleAnnotations attribute, or 0.
    int runtimeVisibleAnnotationsOffset = 0;
    // - The offset of the RuntimeInvisibleAnnotations attribute, or 0.
    int runtimeInvisibleAnnotationsOffset = 0;
    // - The offset of the RuntimeVisibleParameterAnnotations attribute, or 0.
    int runtimeVisibleParameterAnnotationsOffset = 0;
    // - The offset of the RuntimeInvisibleParameterAnnotations attribute, or 0.
    int runtimeInvisibleParameterAnnotationsOffset = 0;
    // - The offset of the RuntimeVisibleTypeAnnotations attribute, or 0.
    int runtimeVisibleTypeAnnotationsOffset = 0;
    // - The offset of the RuntimeInvisibleTypeAnnotations attribute, or 0.
    int runtimeInvisibleTypeAnnotationsOffset = 0;
    // - The offset of the AnnotationDefault attribute, or 0.
    int annotationDefaultOffset = 0;
    // - The offset of the MethodParameters attribute, or 0.
    int methodParametersOffset = 0;
    // - The non standard attributes (linked with their {@link Attribute#nextAttribute} field).
    //   This list in the <i>reverse order</i> or their order in the ClassFile structure.
    Attribute attributes = null;

    int attributesCount = readUnsignedShort(currentOffset);
    currentOffset += 2;
    while (attributesCount-- > 0) {
      // Read the attribute_info's attribute_name and attribute_length fields.
      String attributeName = readUTF8(currentOffset, charBuffer);
      int attributeLength = readInt(currentOffset + 2);
      currentOffset += 6;
      // The tests are sorted in decreasing frequency order (based on frequencies observed on
      // typical classes).
      if (Constants.CODE.equals(attributeName)) {
        if ((context.parsingOptions & SKIP_CODE) == 0) {
          codeOffset = currentOffset;
        }
      }
      else if (Constants.EXCEPTIONS.equals(attributeName)) {
        exceptionsOffset = currentOffset;
        exceptions = new String[readUnsignedShort(exceptionsOffset)];
        int currentExceptionOffset = exceptionsOffset + 2;
        for (int i = 0; i < exceptions.length; ++i) {
          exceptions[i] = readClass(currentExceptionOffset, charBuffer);
          currentExceptionOffset += 2;
        }
      }
      else if (Constants.SIGNATURE.equals(attributeName)) {
        signatureIndex = readUnsignedShort(currentOffset);
      }
      else if (Constants.DEPRECATED.equals(attributeName)) {
        context.currentMethodAccessFlags |= Opcodes.ACC_DEPRECATED;
      }
      else if (Constants.RUNTIME_VISIBLE_ANNOTATIONS.equals(attributeName)) {
        runtimeVisibleAnnotationsOffset = currentOffset;
      }
      else if (Constants.RUNTIME_VISIBLE_TYPE_ANNOTATIONS.equals(attributeName)) {
        runtimeVisibleTypeAnnotationsOffset = currentOffset;
      }
      else if (Constants.ANNOTATION_DEFAULT.equals(attributeName)) {
        annotationDefaultOffset = currentOffset;
      }
      else if (Constants.SYNTHETIC.equals(attributeName)) {
        synthetic = true;
        context.currentMethodAccessFlags |= Opcodes.ACC_SYNTHETIC;
      }
      else if (Constants.RUNTIME_INVISIBLE_ANNOTATIONS.equals(attributeName)) {
        runtimeInvisibleAnnotationsOffset = currentOffset;
      }
      else if (Constants.RUNTIME_INVISIBLE_TYPE_ANNOTATIONS.equals(attributeName)) {
        runtimeInvisibleTypeAnnotationsOffset = currentOffset;
      }
      else if (Constants.RUNTIME_VISIBLE_PARAMETER_ANNOTATIONS.equals(attributeName)) {
        runtimeVisibleParameterAnnotationsOffset = currentOffset;
      }
      else if (Constants.RUNTIME_INVISIBLE_PARAMETER_ANNOTATIONS.equals(attributeName)) {
        runtimeInvisibleParameterAnnotationsOffset = currentOffset;
      }
      else if (Constants.METHOD_PARAMETERS.equals(attributeName)) {
        methodParametersOffset = currentOffset;
      }
      else {
        Attribute attribute =
                readAttribute(
                        context.attributePrototypes,
                        attributeName,
                        currentOffset,
                        attributeLength,
                        charBuffer,
                        -1,
                        null);
        attribute.nextAttribute = attributes;
        attributes = attribute;
      }
      currentOffset += attributeLength;
    }

    // Visit the method declaration.
    MethodVisitor methodVisitor =
            classVisitor.visitMethod(
                    context.currentMethodAccessFlags,
                    context.currentMethodName,
                    context.currentMethodDescriptor,
                    signatureIndex == 0 ? null : readUtf(signatureIndex, charBuffer),
                    exceptions);
    if (methodVisitor == null) {
      return currentOffset;
    }

    // If the returned MethodVisitor is in fact a MethodWriter, it means there is no method
    // adapter between the reader and the writer. In this case, it might be possible to copy
    // the method attributes directly into the writer. If so, return early without visiting
    // the content of these attributes.
    if (methodVisitor instanceof MethodWriter methodWriter) {
      if (methodWriter.canCopyMethodAttributes(
              this,
              synthetic,
              (context.currentMethodAccessFlags & Opcodes.ACC_DEPRECATED) != 0,
              readUnsignedShort(methodInfoOffset + 4),
              signatureIndex,
              exceptionsOffset)) {
        methodWriter.setMethodAttributesSource(methodInfoOffset, currentOffset - methodInfoOffset);
        return currentOffset;
      }
    }

    // Visit the MethodParameters attribute.
    if (methodParametersOffset != 0 && (context.parsingOptions & SKIP_DEBUG) == 0) {
      int parametersCount = readByte(methodParametersOffset);
      int currentParameterOffset = methodParametersOffset + 1;
      while (parametersCount-- > 0) {
        // Read the name_index and access_flags fields and visit them.
        methodVisitor.visitParameter(
                readUTF8(currentParameterOffset, charBuffer),
                readUnsignedShort(currentParameterOffset + 2));
        currentParameterOffset += 4;
      }
    }

    // Visit the AnnotationDefault attribute.
    if (annotationDefaultOffset != 0) {
      AnnotationVisitor annotationVisitor = methodVisitor.visitAnnotationDefault();
      readElementValue(annotationVisitor, annotationDefaultOffset, null, charBuffer);
      if (annotationVisitor != null) {
        annotationVisitor.visitEnd();
      }
    }

    // Visit the RuntimeVisibleAnnotations attribute.
    if (runtimeVisibleAnnotationsOffset != 0) {
      int numAnnotations = readUnsignedShort(runtimeVisibleAnnotationsOffset);
      int currentAnnotationOffset = runtimeVisibleAnnotationsOffset + 2;
      while (numAnnotations-- > 0) {
        // Parse the type_index field.
        String annotationDescriptor = readUTF8(currentAnnotationOffset, charBuffer);
        currentAnnotationOffset += 2;
        // Parse num_element_value_pairs and element_value_pairs and visit these values.
        currentAnnotationOffset =
                readElementValues(
                        methodVisitor.visitAnnotation(annotationDescriptor, /* visible = */ true),
                        currentAnnotationOffset,
                        /* named = */ true,
                        charBuffer);
      }
    }

    // Visit the RuntimeInvisibleAnnotations attribute.
    if (runtimeInvisibleAnnotationsOffset != 0) {
      int numAnnotations = readUnsignedShort(runtimeInvisibleAnnotationsOffset);
      int currentAnnotationOffset = runtimeInvisibleAnnotationsOffset + 2;
      while (numAnnotations-- > 0) {
        // Parse the type_index field.
        String annotationDescriptor = readUTF8(currentAnnotationOffset, charBuffer);
        currentAnnotationOffset += 2;
        // Parse num_element_value_pairs and element_value_pairs and visit these values.
        currentAnnotationOffset =
                readElementValues(
                        methodVisitor.visitAnnotation(annotationDescriptor, /* visible = */ false),
                        currentAnnotationOffset,
                        /* named = */ true,
                        charBuffer);
      }
    }

    // Visit the RuntimeVisibleTypeAnnotations attribute.
    if (runtimeVisibleTypeAnnotationsOffset != 0) {
      int numAnnotations = readUnsignedShort(runtimeVisibleTypeAnnotationsOffset);
      int currentAnnotationOffset = runtimeVisibleTypeAnnotationsOffset + 2;
      while (numAnnotations-- > 0) {
        // Parse the target_type, target_info and target_path fields.
        currentAnnotationOffset = readTypeAnnotationTarget(context, currentAnnotationOffset);
        // Parse the type_index field.
        String annotationDescriptor = readUTF8(currentAnnotationOffset, charBuffer);
        currentAnnotationOffset += 2;
        // Parse num_element_value_pairs and element_value_pairs and visit these values.
        currentAnnotationOffset =
                readElementValues(
                        methodVisitor.visitTypeAnnotation(
                                context.currentTypeAnnotationTarget,
                                context.currentTypeAnnotationTargetPath,
                                annotationDescriptor,
                                /* visible = */ true),
                        currentAnnotationOffset,
                        /* named = */ true,
                        charBuffer);
      }
    }

    // Visit the RuntimeInvisibleTypeAnnotations attribute.
    if (runtimeInvisibleTypeAnnotationsOffset != 0) {
      int numAnnotations = readUnsignedShort(runtimeInvisibleTypeAnnotationsOffset);
      int currentAnnotationOffset = runtimeInvisibleTypeAnnotationsOffset + 2;
      while (numAnnotations-- > 0) {
        // Parse the target_type, target_info and target_path fields.
        currentAnnotationOffset = readTypeAnnotationTarget(context, currentAnnotationOffset);
        // Parse the type_index field.
        String annotationDescriptor = readUTF8(currentAnnotationOffset, charBuffer);
        currentAnnotationOffset += 2;
        // Parse num_element_value_pairs and element_value_pairs and visit these values.
        currentAnnotationOffset =
                readElementValues(
                        methodVisitor.visitTypeAnnotation(
                                context.currentTypeAnnotationTarget,
                                context.currentTypeAnnotationTargetPath,
                                annotationDescriptor,
                                /* visible = */ false),
                        currentAnnotationOffset,
                        /* named = */ true,
                        charBuffer);
      }
    }

    // Visit the RuntimeVisibleParameterAnnotations attribute.
    if (runtimeVisibleParameterAnnotationsOffset != 0) {
      readParameterAnnotations(
              methodVisitor, context, runtimeVisibleParameterAnnotationsOffset, /* visible = */ true);
    }

    // Visit the RuntimeInvisibleParameterAnnotations attribute.
    if (runtimeInvisibleParameterAnnotationsOffset != 0) {
      readParameterAnnotations(
              methodVisitor,
              context,
              runtimeInvisibleParameterAnnotationsOffset,
              /* visible = */ false);
    }

    // Visit the non standard attributes.
    while (attributes != null) {
      // Copy and reset the nextAttribute field so that it can also be used in MethodWriter.
      Attribute nextAttribute = attributes.nextAttribute;
      attributes.nextAttribute = null;
      methodVisitor.visitAttribute(attributes);
      attributes = nextAttribute;
    }

    // Visit the Code attribute.
    if (codeOffset != 0) {
      methodVisitor.visitCode();
      readCode(methodVisitor, context, codeOffset);
    }

    // Visit the end of the method.
    methodVisitor.visitEnd();
    return currentOffset;
  }

  // ----------------------------------------------------------------------------------------------
  // Methods to parse a Code attribute
  // ----------------------------------------------------------------------------------------------

  /**
   * Reads a JVMS 'Code' attribute and makes the given visitor visit it.
   *
   * @param methodVisitor the visitor that must visit the Code attribute.
   * @param context information about the class being parsed.
   * @param codeOffset the start offset in {@link #classFileBuffer} of the Code attribute, excluding
   * its attribute_name_index and attribute_length fields.
   */
  private void readCode(final MethodVisitor methodVisitor, final Context context, final int codeOffset) {
    int currentOffset = codeOffset;

    // Read the max_stack, max_locals and code_length fields.
    final byte[] classBuffer = classFileBuffer;
    final char[] charBuffer = context.charBuffer;
    final int maxStack = readUnsignedShort(currentOffset);
    final int maxLocals = readUnsignedShort(currentOffset + 2);
    final int codeLength = readInt(currentOffset + 4);
    currentOffset += 8;
    if (codeLength > classBuffer.length - currentOffset) {
      throw new IllegalArgumentException();
    }

    // Read the bytecode 'code' array to create a label for each referenced instruction.
    final int bytecodeStartOffset = currentOffset;
    final int bytecodeEndOffset = currentOffset + codeLength;
    final Label[] labels = context.currentMethodLabels = new Label[codeLength + 1];
    while (currentOffset < bytecodeEndOffset) {
      final int bytecodeOffset = currentOffset - bytecodeStartOffset;
      final int opcode = classBuffer[currentOffset] & 0xFF;
      switch (opcode) {
        case Opcodes.NOP, Opcodes.ACONST_NULL, Opcodes.ICONST_M1,
             Opcodes.ICONST_0, Opcodes.ICONST_1, Opcodes.ICONST_2, Opcodes.ICONST_3, Opcodes.ICONST_4, Opcodes.ICONST_5,
             Opcodes.LCONST_0, Opcodes.LCONST_1, Opcodes.FCONST_0, Opcodes.FCONST_1, Opcodes.FCONST_2, Opcodes.DCONST_0,
             Opcodes.DCONST_1, Opcodes.IALOAD, Opcodes.LALOAD, Opcodes.FALOAD, Opcodes.DALOAD, Opcodes.AALOAD, Opcodes.BALOAD,
             Opcodes.CALOAD, Opcodes.SALOAD, Opcodes.IASTORE, Opcodes.LASTORE, Opcodes.FASTORE, Opcodes.DASTORE, Opcodes.AASTORE,
             Opcodes.BASTORE, Opcodes.CASTORE, Opcodes.SASTORE, Opcodes.POP, Opcodes.POP2, Opcodes.DUP, Opcodes.DUP_X1, Opcodes.DUP_X2,
             Opcodes.DUP2, Opcodes.DUP2_X1, Opcodes.DUP2_X2, Opcodes.SWAP, Opcodes.IADD, Opcodes.LADD, Opcodes.FADD, Opcodes.DADD,
             Opcodes.ISUB, Opcodes.LSUB, Opcodes.FSUB, Opcodes.DSUB, Opcodes.IMUL, Opcodes.LMUL, Opcodes.FMUL, Opcodes.DMUL,
             Opcodes.IDIV, Opcodes.LDIV, Opcodes.FDIV, Opcodes.DDIV, Opcodes.IREM, Opcodes.LREM, Opcodes.FREM, Opcodes.DREM,
             Opcodes.INEG, Opcodes.LNEG, Opcodes.FNEG, Opcodes.DNEG, Opcodes.ISHL, Opcodes.LSHL, Opcodes.ISHR, Opcodes.LSHR,
             Opcodes.IUSHR, Opcodes.LUSHR, Opcodes.IAND, Opcodes.LAND, Opcodes.IOR, Opcodes.LOR, Opcodes.IXOR, Opcodes.LXOR,
             Opcodes.I2L, Opcodes.I2F, Opcodes.I2D, Opcodes.L2I, Opcodes.L2F, Opcodes.L2D, Opcodes.F2I, Opcodes.F2L, Opcodes.F2D,
             Opcodes.D2I, Opcodes.D2L, Opcodes.D2F, Opcodes.I2B, Opcodes.I2C, Opcodes.I2S, Opcodes.LCMP, Opcodes.FCMPL, Opcodes.FCMPG,
             Opcodes.DCMPL, Opcodes.DCMPG, Opcodes.IRETURN, Opcodes.LRETURN, Opcodes.FRETURN, Opcodes.DRETURN, Opcodes.ARETURN,
             Opcodes.RETURN, Opcodes.ARRAYLENGTH, Opcodes.ATHROW, Opcodes.MONITORENTER, Opcodes.MONITOREXIT, Constants.ILOAD_0,
             Constants.ILOAD_1, Constants.ILOAD_2, Constants.ILOAD_3, Constants.LLOAD_0, Constants.LLOAD_1, Constants.LLOAD_2,
             Constants.LLOAD_3, Constants.FLOAD_0, Constants.FLOAD_1, Constants.FLOAD_2, Constants.FLOAD_3, Constants.DLOAD_0,
             Constants.DLOAD_1, Constants.DLOAD_2, Constants.DLOAD_3, Constants.ALOAD_0, Constants.ALOAD_1, Constants.ALOAD_2,
             Constants.ALOAD_3, Constants.ISTORE_0, Constants.ISTORE_1, Constants.ISTORE_2, Constants.ISTORE_3, Constants.LSTORE_0,
             Constants.LSTORE_1, Constants.LSTORE_2, Constants.LSTORE_3, Constants.FSTORE_0, Constants.FSTORE_1, Constants.FSTORE_2,
             Constants.FSTORE_3, Constants.DSTORE_0, Constants.DSTORE_1, Constants.DSTORE_2, Constants.DSTORE_3, Constants.ASTORE_0,
             Constants.ASTORE_1, Constants.ASTORE_2, Constants.ASTORE_3 -> currentOffset += 1;

        case Opcodes.IFEQ, Opcodes.IFNE, Opcodes.IFLT, Opcodes.IFGE, Opcodes.IFGT, Opcodes.IFLE, Opcodes.IF_ICMPEQ, Opcodes.IF_ICMPNE,
             Opcodes.IF_ICMPLT, Opcodes.IF_ICMPGE, Opcodes.IF_ICMPGT, Opcodes.IF_ICMPLE, Opcodes.IF_ACMPEQ, Opcodes.IF_ACMPNE, Opcodes.GOTO,
             Opcodes.JSR, Opcodes.IFNULL, Opcodes.IFNONNULL -> {
          createLabel(bytecodeOffset + readShort(currentOffset + 1), labels);
          currentOffset += 3;
        }

        case Constants.ASM_IFEQ, Constants.ASM_IFNE, Constants.ASM_IFLT, Constants.ASM_IFGE, Constants.ASM_IFGT, Constants.ASM_IFLE,
             Constants.ASM_IF_ICMPEQ, Constants.ASM_IF_ICMPNE, Constants.ASM_IF_ICMPLT, Constants.ASM_IF_ICMPGE, Constants.ASM_IF_ICMPGT,
             Constants.ASM_IF_ICMPLE, Constants.ASM_IF_ACMPEQ, Constants.ASM_IF_ACMPNE, Constants.ASM_GOTO, Constants.ASM_JSR,
             Constants.ASM_IFNULL, Constants.ASM_IFNONNULL -> {
          createLabel(bytecodeOffset + readUnsignedShort(currentOffset + 1), labels);
          currentOffset += 3;
        }
        case Constants.GOTO_W, Constants.JSR_W, Constants.ASM_GOTO_W -> {
          createLabel(bytecodeOffset + readInt(currentOffset + 1), labels);
          currentOffset += 5;
        }
        case Constants.WIDE -> {
          switch (classBuffer[currentOffset + 1] & 0xFF) {
            case Opcodes.ILOAD, Opcodes.FLOAD, Opcodes.ALOAD, Opcodes.LLOAD, Opcodes.DLOAD, Opcodes.ISTORE,
                 Opcodes.FSTORE, Opcodes.ASTORE, Opcodes.LSTORE, Opcodes.DSTORE, Opcodes.RET -> currentOffset += 4;
            case Opcodes.IINC -> currentOffset += 6;
            default -> throw new IllegalArgumentException();
          }
        }
        case Opcodes.TABLESWITCH -> {
          // Skip 0 to 3 padding bytes.
          currentOffset += 4 - (bytecodeOffset & 3);
          // Read the default label and the number of table entries.
          createLabel(bytecodeOffset + readInt(currentOffset), labels);
          int numTableEntries = readInt(currentOffset + 8) - readInt(currentOffset + 4) + 1;
          currentOffset += 12;
          // Read the table labels.
          while (numTableEntries-- > 0) {
            createLabel(bytecodeOffset + readInt(currentOffset), labels);
            currentOffset += 4;
          }
        }
        case Opcodes.LOOKUPSWITCH -> {
          // Skip 0 to 3 padding bytes.
          currentOffset += 4 - (bytecodeOffset & 3);
          // Read the default label and the number of switch cases.
          createLabel(bytecodeOffset + readInt(currentOffset), labels);
          int numSwitchCases = readInt(currentOffset + 4);
          currentOffset += 8;
          // Read the switch labels.
          while (numSwitchCases-- > 0) {
            createLabel(bytecodeOffset + readInt(currentOffset + 4), labels);
            currentOffset += 8;
          }
        }
        case Opcodes.ILOAD, Opcodes.LLOAD, Opcodes.FLOAD, Opcodes.DLOAD, Opcodes.ALOAD, Opcodes.ISTORE, Opcodes.LSTORE, Opcodes.FSTORE,
             Opcodes.DSTORE, Opcodes.ASTORE, Opcodes.RET, Opcodes.BIPUSH, Opcodes.NEWARRAY, Opcodes.LDC -> currentOffset += 2;

        case Opcodes.SIPUSH, Constants.LDC_W, Constants.LDC2_W, Opcodes.GETSTATIC, Opcodes.PUTSTATIC, Opcodes.GETFIELD,
             Opcodes.PUTFIELD, Opcodes.INVOKEVIRTUAL, Opcodes.INVOKESPECIAL, Opcodes.INVOKESTATIC, Opcodes.NEW, Opcodes.ANEWARRAY,
             Opcodes.CHECKCAST, Opcodes.INSTANCEOF, Opcodes.IINC -> currentOffset += 3;

        case Opcodes.INVOKEINTERFACE, Opcodes.INVOKEDYNAMIC -> currentOffset += 5;
        case Opcodes.MULTIANEWARRAY -> currentOffset += 4;
        default -> throw new IllegalArgumentException();
      }
    }

    // Read the 'exception_table_length' and 'exception_table' field to create a label for each
    // referenced instruction, and to make methodVisitor visit the corresponding try catch blocks.
    int exceptionTableLength = readUnsignedShort(currentOffset);
    currentOffset += 2;
    final int[] cpInfoOffsets = this.cpInfoOffsets;
    while (exceptionTableLength-- > 0) {
      Label start = createLabel(readUnsignedShort(currentOffset), labels);
      Label end = createLabel(readUnsignedShort(currentOffset + 2), labels);
      Label handler = createLabel(readUnsignedShort(currentOffset + 4), labels);
      String catchType = readUTF8(cpInfoOffsets[readUnsignedShort(currentOffset + 6)], charBuffer);
      currentOffset += 8;
      methodVisitor.visitTryCatchBlock(start, end, handler, catchType);
    }

    // Read the Code attributes to create a label for each referenced instruction (the variables
    // are ordered as in Section 4.7 of the JVMS). Attribute offsets exclude the
    // attribute_name_index and attribute_length fields.
    // - The offset of the current 'stack_map_frame' in the StackMap[Table] attribute, or 0.
    // Initially, this is the offset of the first 'stack_map_frame' entry. Then this offset is
    // updated after each stack_map_frame is read.
    int stackMapFrameOffset = 0;
    // - The end offset of the StackMap[Table] attribute, or 0.
    int stackMapTableEndOffset = 0;
    // - Whether the stack map frames are compressed (i.e. in a StackMapTable) or not.
    boolean compressedFrames = true;
    // - The offset of the LocalVariableTable attribute, or 0.
    int localVariableTableOffset = 0;
    // - The offset of the LocalVariableTypeTable attribute, or 0.
    int localVariableTypeTableOffset = 0;
    // - The offset of each 'type_annotation' entry in the RuntimeVisibleTypeAnnotations
    // attribute, or null.
    int[] visibleTypeAnnotationOffsets = null;
    // - The offset of each 'type_annotation' entry in the RuntimeInvisibleTypeAnnotations
    // attribute, or null.
    int[] invisibleTypeAnnotationOffsets = null;
    // - The non standard attributes (linked with their {@link Attribute#nextAttribute} field).
    //   This list in the <i>reverse order</i> or their order in the ClassFile structure.
    Attribute attributes = null;

    int attributesCount = readUnsignedShort(currentOffset);
    currentOffset += 2;
    while (attributesCount-- > 0) {
      // Read the attribute_info's attribute_name and attribute_length fields.
      String attributeName = readUTF8(currentOffset, charBuffer);
      int attributeLength = readInt(currentOffset + 2);
      currentOffset += 6;
      if (Constants.LOCAL_VARIABLE_TABLE.equals(attributeName)) {
        if ((context.parsingOptions & SKIP_DEBUG) == 0) {
          localVariableTableOffset = currentOffset;
          // Parse the attribute to find the corresponding (debug only) labels.
          int currentLocalVariableTableOffset = currentOffset;
          int localVariableTableLength = readUnsignedShort(currentLocalVariableTableOffset);
          currentLocalVariableTableOffset += 2;
          while (localVariableTableLength-- > 0) {
            int startPc = readUnsignedShort(currentLocalVariableTableOffset);
            createDebugLabel(startPc, labels);
            int length = readUnsignedShort(currentLocalVariableTableOffset + 2);
            createDebugLabel(startPc + length, labels);
            // Skip the name_index, descriptor_index and index fields (2 bytes each).
            currentLocalVariableTableOffset += 10;
          }
        }
      }
      else if (Constants.LOCAL_VARIABLE_TYPE_TABLE.equals(attributeName)) {
        localVariableTypeTableOffset = currentOffset;
        // Here we do not extract the labels corresponding to the attribute content. We assume they
        // are the same or a subset of those of the LocalVariableTable attribute.
      }
      else if (Constants.LINE_NUMBER_TABLE.equals(attributeName)) {
        if ((context.parsingOptions & SKIP_DEBUG) == 0) {
          // Parse the attribute to find the corresponding (debug only) labels.
          int currentLineNumberTableOffset = currentOffset;
          int lineNumberTableLength = readUnsignedShort(currentLineNumberTableOffset);
          currentLineNumberTableOffset += 2;
          while (lineNumberTableLength-- > 0) {
            int startPc = readUnsignedShort(currentLineNumberTableOffset);
            int lineNumber = readUnsignedShort(currentLineNumberTableOffset + 2);
            currentLineNumberTableOffset += 4;
            createDebugLabel(startPc, labels);
            labels[startPc].addLineNumber(lineNumber);
          }
        }
      }
      else if (Constants.RUNTIME_VISIBLE_TYPE_ANNOTATIONS.equals(attributeName)) {
        visibleTypeAnnotationOffsets = readTypeAnnotations(methodVisitor, context, currentOffset, /* visible = */ true);
        // Here we do not extract the labels corresponding to the attribute content. This would
        // require a full parsing of the attribute, which would need to be repeated when parsing
        // the bytecode instructions (see below). Instead, the content of the attribute is read one
        // type annotation at a time (i.e. after a type annotation has been visited, the next type
        // annotation is read), and the labels it contains are also extracted one annotation at a
        // time. This assumes that type annotations are ordered by increasing bytecode offset.
      }
      else if (Constants.RUNTIME_INVISIBLE_TYPE_ANNOTATIONS.equals(attributeName)) {
        invisibleTypeAnnotationOffsets = readTypeAnnotations(methodVisitor, context, currentOffset, /* visible = */ false);
        // Same comment as above for the RuntimeVisibleTypeAnnotations attribute.
      }
      else if (Constants.STACK_MAP_TABLE.equals(attributeName)) {
        if ((context.parsingOptions & SKIP_FRAMES) == 0) {
          stackMapFrameOffset = currentOffset + 2;
          stackMapTableEndOffset = currentOffset + attributeLength;
        }
        // Here we do not extract the labels corresponding to the attribute content. This would
        // require a full parsing of the attribute, which would need to be repeated when parsing
        // the bytecode instructions (see below). Instead, the content of the attribute is read one
        // frame at a time (i.e. after a frame has been visited, the next frame is read), and the
        // labels it contains are also extracted one frame at a time. Thanks to the ordering of
        // frames, having only a "one frame lookahead" is not a problem, i.e. it is not possible to
        // see an offset smaller than the offset of the current instruction and for which no Label
        // exist. Except for UNINITIALIZED type offsets. We solve this by parsing the stack map
        // table without a full decoding (see below).
      }
      else if ("StackMap".equals(attributeName)) {
        if ((context.parsingOptions & SKIP_FRAMES) == 0) {
          stackMapFrameOffset = currentOffset + 2;
          stackMapTableEndOffset = currentOffset + attributeLength;
          compressedFrames = false;
        }
        // IMPORTANT! Here we assume that the frames are ordered, as in the StackMapTable attribute,
        // although this is not guaranteed by the attribute format. This allows an incremental
        // extraction of the labels corresponding to this attribute (see the comment above for the
        // StackMapTable attribute).
      }
      else {
        Attribute attribute = readAttribute(context.attributePrototypes, attributeName,
                currentOffset, attributeLength, charBuffer, codeOffset, labels);
        attribute.nextAttribute = attributes;
        attributes = attribute;
      }
      currentOffset += attributeLength;
    }

    // Initialize the context fields related to stack map frames, and generate the first
    // (implicit) stack map frame, if needed.
    final boolean expandFrames = (context.parsingOptions & EXPAND_FRAMES) != 0;
    if (stackMapFrameOffset != 0) {
      // The bytecode offset of the first explicit frame is not offset_delta + 1 but only
      // offset_delta. Setting the implicit frame offset to -1 allows us to use of the
      // "offset_delta + 1" rule in all cases.
      context.currentFrameOffset = -1;
      context.currentFrameType = 0;
      context.currentFrameLocalCount = 0;
      context.currentFrameLocalCountDelta = 0;
      context.currentFrameLocalTypes = new Object[maxLocals];
      context.currentFrameStackCount = 0;
      context.currentFrameStackTypes = new Object[maxStack];
      if (expandFrames) {
        computeImplicitFrame(context);
      }
      // Find the labels for UNINITIALIZED frame types. Instead of decoding each element of the
      // stack map table, we look for 3 consecutive bytes that "look like" an UNINITIALIZED type
      // (tag ITEM_Uninitialized, offset within bytecode bounds, NEW instruction at this offset).
      // We may find false positives (i.e. not real UNINITIALIZED types), but this should be rare,
      // and the only consequence will be the creation of an unneeded label. This is better than
      // creating a label for each NEW instruction, and faster than fully decoding the whole stack
      // map table.
      for (int offset = stackMapFrameOffset; offset < stackMapTableEndOffset - 2; ++offset) {
        if (classBuffer[offset] == Frame.ITEM_UNINITIALIZED) {
          int potentialBytecodeOffset = readUnsignedShort(offset + 1);
          if (potentialBytecodeOffset >= 0
                  && potentialBytecodeOffset < codeLength
                  && (classBuffer[bytecodeStartOffset + potentialBytecodeOffset] & 0xFF) == Opcodes.NEW) {
            createLabel(potentialBytecodeOffset, labels);
          }
        }
      }
    }
    if (expandFrames && (context.parsingOptions & EXPAND_ASM_INSNS) != 0) {
      // Expanding the ASM specific instructions can introduce F_INSERT frames, even if the method
      // does not currently have any frame. These inserted frames must be computed by simulating the
      // effect of the bytecode instructions, one by one, starting from the implicit first frame.
      // For this, MethodWriter needs to know maxLocals before the first instruction is visited. To
      // ensure this, we visit the implicit first frame here (passing only maxLocals - the rest is
      // computed in MethodWriter).
      methodVisitor.visitFrame(Opcodes.F_NEW, maxLocals, null, 0, null);
    }

    // Visit the bytecode instructions. First, introduce state variables for the incremental parsing
    // of the type annotations.

    // Index of the next runtime visible type annotation to read (in the
    // visibleTypeAnnotationOffsets array).
    int currentVisibleTypeAnnotationIndex = 0;
    // The bytecode offset of the next runtime visible type annotation to read, or -1.
    int currentVisibleTypeAnnotationBytecodeOffset = getTypeAnnotationBytecodeOffset(visibleTypeAnnotationOffsets, 0);
    // Index of the next runtime invisible type annotation to read (in the
    // invisibleTypeAnnotationOffsets array).
    int currentInvisibleTypeAnnotationIndex = 0;
    // The bytecode offset of the next runtime invisible type annotation to read, or -1.
    int currentInvisibleTypeAnnotationBytecodeOffset = getTypeAnnotationBytecodeOffset(invisibleTypeAnnotationOffsets, 0);

    // Whether a F_INSERT stack map frame must be inserted before the current instruction.
    boolean insertFrame = false;

    // The delta to subtract from a goto_w or jsr_w opcode to get the corresponding goto or jsr
    // opcode, or 0 if goto_w and jsr_w must be left unchanged (i.e. when expanding ASM specific
    // instructions).
    final int wideJumpOpcodeDelta =
            (context.parsingOptions & EXPAND_ASM_INSNS) == 0 ? Constants.WIDE_JUMP_OPCODE_DELTA : 0;

    currentOffset = bytecodeStartOffset;
    while (currentOffset < bytecodeEndOffset) {
      final int currentBytecodeOffset = currentOffset - bytecodeStartOffset;

      // Visit the label and the line number(s) for this bytecode offset, if any.
      Label currentLabel = labels[currentBytecodeOffset];
      if (currentLabel != null) {
        currentLabel.accept(methodVisitor, (context.parsingOptions & SKIP_DEBUG) == 0);
      }

      // Visit the stack map frame for this bytecode offset, if any.
      while (stackMapFrameOffset != 0
              && (context.currentFrameOffset == currentBytecodeOffset || context.currentFrameOffset == -1)) {
        // If there is a stack map frame for this offset, make methodVisitor visit it, and read the
        // next stack map frame if there is one.
        if (context.currentFrameOffset != -1) {
          if (!compressedFrames || expandFrames) {
            methodVisitor.visitFrame(Opcodes.F_NEW, context.currentFrameLocalCount, context.currentFrameLocalTypes,
                    context.currentFrameStackCount, context.currentFrameStackTypes);
          }
          else {
            methodVisitor.visitFrame(context.currentFrameType, context.currentFrameLocalCountDelta,
                    context.currentFrameLocalTypes, context.currentFrameStackCount, context.currentFrameStackTypes);
          }
          // Since there is already a stack map frame for this bytecode offset, there is no need to
          // insert a new one.
          insertFrame = false;
        }
        if (stackMapFrameOffset < stackMapTableEndOffset) {
          stackMapFrameOffset = readStackMapFrame(stackMapFrameOffset, compressedFrames, expandFrames, context);
        }
        else {
          stackMapFrameOffset = 0;
        }
      }

      // Insert a stack map frame for this bytecode offset, if requested by setting insertFrame to
      // true during the previous iteration. The actual frame content is computed in MethodWriter.
      if (insertFrame) {
        if ((context.parsingOptions & EXPAND_FRAMES) != 0) {
          methodVisitor.visitFrame(Constants.F_INSERT, 0, null, 0, null);
        }
        insertFrame = false;
      }

      // Visit the instruction at this bytecode offset.
      int opcode = classBuffer[currentOffset] & 0xFF;
      switch (opcode) {
        case Opcodes.NOP, Opcodes.ACONST_NULL, Opcodes.ICONST_M1, Opcodes.ICONST_0, Opcodes.ICONST_1, Opcodes.ICONST_2, Opcodes.ICONST_3,
             Opcodes.ICONST_4, Opcodes.ICONST_5, Opcodes.LCONST_0, Opcodes.LCONST_1, Opcodes.FCONST_0, Opcodes.FCONST_1, Opcodes.FCONST_2,
             Opcodes.DCONST_0, Opcodes.DCONST_1, Opcodes.IALOAD, Opcodes.LALOAD, Opcodes.FALOAD, Opcodes.DALOAD, Opcodes.AALOAD, Opcodes.BALOAD,
             Opcodes.CALOAD, Opcodes.SALOAD, Opcodes.IASTORE, Opcodes.LASTORE, Opcodes.FASTORE, Opcodes.DASTORE, Opcodes.AASTORE, Opcodes.BASTORE,
             Opcodes.CASTORE, Opcodes.SASTORE, Opcodes.POP, Opcodes.POP2, Opcodes.DUP, Opcodes.DUP_X1, Opcodes.DUP_X2, Opcodes.DUP2,
             Opcodes.DUP2_X1, Opcodes.DUP2_X2, Opcodes.SWAP, Opcodes.IADD, Opcodes.LADD, Opcodes.FADD, Opcodes.DADD, Opcodes.ISUB,
             Opcodes.LSUB, Opcodes.FSUB, Opcodes.DSUB, Opcodes.IMUL, Opcodes.LMUL, Opcodes.FMUL, Opcodes.DMUL, Opcodes.IDIV, Opcodes.LDIV,
             Opcodes.FDIV, Opcodes.DDIV, Opcodes.IREM, Opcodes.LREM, Opcodes.FREM, Opcodes.DREM, Opcodes.INEG, Opcodes.LNEG, Opcodes.FNEG,
             Opcodes.DNEG, Opcodes.ISHL, Opcodes.LSHL, Opcodes.ISHR, Opcodes.LSHR, Opcodes.IUSHR, Opcodes.LUSHR, Opcodes.IAND, Opcodes.LAND,
             Opcodes.IOR, Opcodes.LOR, Opcodes.IXOR, Opcodes.LXOR, Opcodes.I2L, Opcodes.I2F, Opcodes.I2D, Opcodes.L2I, Opcodes.L2F, Opcodes.L2D,
             Opcodes.F2I, Opcodes.F2L, Opcodes.F2D, Opcodes.D2I, Opcodes.D2L, Opcodes.D2F, Opcodes.I2B, Opcodes.I2C, Opcodes.I2S, Opcodes.LCMP,
             Opcodes.FCMPL, Opcodes.FCMPG, Opcodes.DCMPL, Opcodes.DCMPG, Opcodes.IRETURN, Opcodes.LRETURN, Opcodes.FRETURN, Opcodes.DRETURN,
             Opcodes.ARETURN, Opcodes.RETURN, Opcodes.ARRAYLENGTH, Opcodes.ATHROW, Opcodes.MONITORENTER, Opcodes.MONITOREXIT -> {
          methodVisitor.visitInsn(opcode);
          currentOffset += 1;
        }
        case Constants.ILOAD_0, Constants.ILOAD_1, Constants.ILOAD_2, Constants.ILOAD_3, Constants.LLOAD_0, Constants.LLOAD_1, Constants.LLOAD_2,
             Constants.LLOAD_3, Constants.FLOAD_0, Constants.FLOAD_1, Constants.FLOAD_2, Constants.FLOAD_3, Constants.DLOAD_0, Constants.DLOAD_1,
             Constants.DLOAD_2, Constants.DLOAD_3, Constants.ALOAD_0, Constants.ALOAD_1, Constants.ALOAD_2, Constants.ALOAD_3 -> {
          opcode -= Constants.ILOAD_0;
          methodVisitor.visitVarInsn(Opcodes.ILOAD + (opcode >> 2), opcode & 0x3);
          currentOffset += 1;
        }
        case Constants.ISTORE_0, Constants.ISTORE_1, Constants.ISTORE_2, Constants.ISTORE_3, Constants.LSTORE_0, Constants.LSTORE_1, Constants.LSTORE_2,
             Constants.LSTORE_3, Constants.FSTORE_0, Constants.FSTORE_1, Constants.FSTORE_2, Constants.FSTORE_3, Constants.DSTORE_0,
             Constants.DSTORE_1, Constants.DSTORE_2, Constants.DSTORE_3, Constants.ASTORE_0, Constants.ASTORE_1, Constants.ASTORE_2,
             Constants.ASTORE_3 -> {
          opcode -= Constants.ISTORE_0;
          methodVisitor.visitVarInsn(Opcodes.ISTORE + (opcode >> 2), opcode & 0x3);
          currentOffset += 1;
        }
        case Opcodes.IFEQ, Opcodes.IFNE, Opcodes.IFLT, Opcodes.IFGE, Opcodes.IFGT, Opcodes.IFLE, Opcodes.IF_ICMPEQ, Opcodes.IF_ICMPNE,
             Opcodes.IF_ICMPLT, Opcodes.IF_ICMPGE, Opcodes.IF_ICMPGT, Opcodes.IF_ICMPLE, Opcodes.IF_ACMPEQ, Opcodes.IF_ACMPNE, Opcodes.GOTO,
             Opcodes.JSR, Opcodes.IFNULL, Opcodes.IFNONNULL -> {
          methodVisitor.visitJumpInsn(opcode, labels[currentBytecodeOffset + readShort(currentOffset + 1)]);
          currentOffset += 3;
        }
        case Constants.GOTO_W, Constants.JSR_W -> {
          methodVisitor.visitJumpInsn(opcode - wideJumpOpcodeDelta,
                  labels[currentBytecodeOffset + readInt(currentOffset + 1)]);
          currentOffset += 5;
        }
        case Constants.ASM_IFEQ, Constants.ASM_IFNE, Constants.ASM_IFLT, Constants.ASM_IFGE, Constants.ASM_IFGT, Constants.ASM_IFLE, Constants.ASM_IF_ICMPEQ, Constants.ASM_IF_ICMPNE,
             Constants.ASM_IF_ICMPLT, Constants.ASM_IF_ICMPGE, Constants.ASM_IF_ICMPGT, Constants.ASM_IF_ICMPLE, Constants.ASM_IF_ACMPEQ, Constants.ASM_IF_ACMPNE, Constants.ASM_GOTO,
             Constants.ASM_JSR, Constants.ASM_IFNULL, Constants.ASM_IFNONNULL -> {
          // A forward jump with an offset > 32767. In this case we automatically replace ASM_GOTO
          // with GOTO_W, ASM_JSR with JSR_W and ASM_IFxxx <l> with IFNOTxxx <L> GOTO_W <l> L:...,
          // where IFNOTxxx is the "opposite" opcode of ASMS_IFxxx (e.g. IFNE for ASM_IFEQ) and
          // where <L> designates the instruction just after the GOTO_W.
          // First, change the ASM specific opcodes ASM_IFEQ ... ASM_JSR, ASM_IFNULL and
          // ASM_IFNONNULL to IFEQ ... JSR, IFNULL and IFNONNULL.
          opcode = opcode < Constants.ASM_IFNULL ? opcode - Constants.ASM_OPCODE_DELTA : opcode - Constants.ASM_IFNULL_OPCODE_DELTA;
          Label target = labels[currentBytecodeOffset + readUnsignedShort(currentOffset + 1)];
          if (opcode == Opcodes.GOTO || opcode == Opcodes.JSR) {
            // Replace GOTO with GOTO_W and JSR with JSR_W.
            methodVisitor.visitJumpInsn(opcode + Constants.WIDE_JUMP_OPCODE_DELTA, target);
          }
          else {
            // Compute the "opposite" of opcode. This can be done by flipping the least
            // significant bit for IFNULL and IFNONNULL, and similarly for IFEQ ... IF_ACMPEQ
            // (with a pre and post offset by 1).
            opcode = opcode < Opcodes.GOTO ? ((opcode + 1) ^ 1) - 1 : opcode ^ 1;
            Label endif = createLabel(currentBytecodeOffset + 3, labels);
            methodVisitor.visitJumpInsn(opcode, endif);
            methodVisitor.visitJumpInsn(Constants.GOTO_W, target);
            // endif designates the instruction just after GOTO_W, and is visited as part of the
            // next instruction. Since it is a jump target, we need to insert a frame here.
            insertFrame = true;
          }
          currentOffset += 3;
        }
        case Constants.ASM_GOTO_W -> {
          // Replace ASM_GOTO_W with GOTO_W.
          methodVisitor.visitJumpInsn(Constants.GOTO_W, labels[currentBytecodeOffset + readInt(currentOffset + 1)]);
          // The instruction just after is a jump target (because ASM_GOTO_W is used in patterns
          // IFNOTxxx <L> ASM_GOTO_W <l> L:..., see MethodWriter), so we need to insert a frame
          // here.
          insertFrame = true;
          currentOffset += 5;
        }
        case Constants.WIDE -> {
          opcode = classBuffer[currentOffset + 1] & 0xFF;
          if (opcode == Opcodes.IINC) {
            methodVisitor.visitIincInsn(readUnsignedShort(currentOffset + 2), readShort(currentOffset + 4));
            currentOffset += 6;
          }
          else {
            methodVisitor.visitVarInsn(opcode, readUnsignedShort(currentOffset + 2));
            currentOffset += 4;
          }
        }
        case Opcodes.TABLESWITCH -> {
          // Skip 0 to 3 padding bytes.
          currentOffset += 4 - (currentBytecodeOffset & 3);
          // Read the instruction.
          Label defaultLabel = labels[currentBytecodeOffset + readInt(currentOffset)];
          int low = readInt(currentOffset + 4);
          int high = readInt(currentOffset + 8);
          currentOffset += 12;
          Label[] table = new Label[high - low + 1];
          for (int i = 0; i < table.length; ++i) {
            table[i] = labels[currentBytecodeOffset + readInt(currentOffset)];
            currentOffset += 4;
          }
          methodVisitor.visitTableSwitchInsn(low, high, defaultLabel, table);
        }
        case Opcodes.LOOKUPSWITCH -> {
          // Skip 0 to 3 padding bytes.
          currentOffset += 4 - (currentBytecodeOffset & 3);
          // Read the instruction.
          Label defaultLabel = labels[currentBytecodeOffset + readInt(currentOffset)];
          int numPairs = readInt(currentOffset + 4);
          currentOffset += 8;
          int[] keys = new int[numPairs];
          Label[] values = new Label[numPairs];
          for (int i = 0; i < numPairs; ++i) {
            keys[i] = readInt(currentOffset);
            values[i] = labels[currentBytecodeOffset + readInt(currentOffset + 4)];
            currentOffset += 8;
          }
          methodVisitor.visitLookupSwitchInsn(defaultLabel, keys, values);
        }
        case Opcodes.ILOAD, Opcodes.LLOAD, Opcodes.FLOAD, Opcodes.DLOAD, Opcodes.ALOAD, Opcodes.ISTORE, Opcodes.LSTORE, Opcodes.FSTORE,
             Opcodes.DSTORE, Opcodes.ASTORE, Opcodes.RET -> {
          methodVisitor.visitVarInsn(opcode, classBuffer[currentOffset + 1] & 0xFF);
          currentOffset += 2;
        }
        case Opcodes.BIPUSH, Opcodes.NEWARRAY -> {
          methodVisitor.visitIntInsn(opcode, classBuffer[currentOffset + 1]);
          currentOffset += 2;
        }
        case Opcodes.SIPUSH -> {
          methodVisitor.visitIntInsn(opcode, readShort(currentOffset + 1));
          currentOffset += 3;
        }
        case Opcodes.LDC -> {
          methodVisitor.visitLdcInsn(readConst(classBuffer[currentOffset + 1] & 0xFF, charBuffer));
          currentOffset += 2;
        }
        case Constants.LDC_W, Constants.LDC2_W -> {
          methodVisitor.visitLdcInsn(readConst(readUnsignedShort(currentOffset + 1), charBuffer));
          currentOffset += 3;
        }
        case Opcodes.GETSTATIC, Opcodes.PUTSTATIC, Opcodes.GETFIELD, Opcodes.PUTFIELD, Opcodes.INVOKEVIRTUAL, Opcodes.INVOKESPECIAL,
             Opcodes.INVOKESTATIC, Opcodes.INVOKEINTERFACE -> {
          int cpInfoOffset = cpInfoOffsets[readUnsignedShort(currentOffset + 1)];
          int nameAndTypeCpInfoOffset = cpInfoOffsets[readUnsignedShort(cpInfoOffset + 2)];
          String owner = readClass(cpInfoOffset, charBuffer);
          String name = readUTF8(nameAndTypeCpInfoOffset, charBuffer);
          String descriptor = readUTF8(nameAndTypeCpInfoOffset + 2, charBuffer);
          if (opcode < Opcodes.INVOKEVIRTUAL) {
            methodVisitor.visitFieldInsn(opcode, owner, name, descriptor);
          }
          else {
            boolean isInterface = classBuffer[cpInfoOffset - 1] == Symbol.CONSTANT_INTERFACE_METHODREF_TAG;
            methodVisitor.visitMethodInsn(opcode, owner, name, descriptor, isInterface);
          }
          if (opcode == Opcodes.INVOKEINTERFACE) {
            currentOffset += 5;
          }
          else {
            currentOffset += 3;
          }
        }
        case Opcodes.INVOKEDYNAMIC -> {
          int cpInfoOffset = cpInfoOffsets[readUnsignedShort(currentOffset + 1)];
          int nameAndTypeCpInfoOffset = cpInfoOffsets[readUnsignedShort(cpInfoOffset + 2)];
          String name = readUTF8(nameAndTypeCpInfoOffset, charBuffer);
          String descriptor = readUTF8(nameAndTypeCpInfoOffset + 2, charBuffer);
          int bootstrapMethodOffset = bootstrapMethodOffsets[readUnsignedShort(cpInfoOffset)];
          Handle handle = (Handle) readConst(readUnsignedShort(bootstrapMethodOffset), charBuffer);
          Object[] bootstrapMethodArguments = new Object[readUnsignedShort(bootstrapMethodOffset + 2)];
          bootstrapMethodOffset += 4;
          for (int i = 0; i < bootstrapMethodArguments.length; i++) {
            bootstrapMethodArguments[i] = readConst(readUnsignedShort(bootstrapMethodOffset), charBuffer);
            bootstrapMethodOffset += 2;
          }
          methodVisitor.visitInvokeDynamicInsn(name, descriptor, handle, bootstrapMethodArguments);
          currentOffset += 5;
        }
        case Opcodes.NEW, Opcodes.ANEWARRAY, Opcodes.CHECKCAST, Opcodes.INSTANCEOF -> {
          methodVisitor.visitTypeInsn(opcode, readClass(currentOffset + 1, charBuffer));
          currentOffset += 3;
        }
        case Opcodes.IINC -> {
          methodVisitor.visitIincInsn(classBuffer[currentOffset + 1] & 0xFF, classBuffer[currentOffset + 2]);
          currentOffset += 3;
        }
        case Opcodes.MULTIANEWARRAY -> {
          methodVisitor.visitMultiANewArrayInsn(
                  readClass(currentOffset + 1, charBuffer), classBuffer[currentOffset + 3] & 0xFF);
          currentOffset += 4;
        }
        default -> throw new AssertionError();
      }

      // Visit the runtime visible instruction annotations, if any.
      while (visibleTypeAnnotationOffsets != null
              && currentVisibleTypeAnnotationIndex < visibleTypeAnnotationOffsets.length
              && currentVisibleTypeAnnotationBytecodeOffset <= currentBytecodeOffset) {
        if (currentVisibleTypeAnnotationBytecodeOffset == currentBytecodeOffset) {
          // Parse the target_type, target_info and target_path fields.
          int currentAnnotationOffset = readTypeAnnotationTarget(
                  context, visibleTypeAnnotationOffsets[currentVisibleTypeAnnotationIndex]);
          // Parse the type_index field.
          String annotationDescriptor = readUTF8(currentAnnotationOffset, charBuffer);
          currentAnnotationOffset += 2;
          // Parse num_element_value_pairs and element_value_pairs and visit these values.
          readElementValues(methodVisitor.visitInsnAnnotation(context.currentTypeAnnotationTarget,
                          context.currentTypeAnnotationTargetPath, annotationDescriptor,/* visible = */ true),
                  currentAnnotationOffset,/* named = */ true, charBuffer);
        }
        currentVisibleTypeAnnotationBytecodeOffset = getTypeAnnotationBytecodeOffset(visibleTypeAnnotationOffsets, ++currentVisibleTypeAnnotationIndex);
      }

      // Visit the runtime invisible instruction annotations, if any.
      while (invisibleTypeAnnotationOffsets != null
              && currentInvisibleTypeAnnotationIndex < invisibleTypeAnnotationOffsets.length
              && currentInvisibleTypeAnnotationBytecodeOffset <= currentBytecodeOffset) {
        if (currentInvisibleTypeAnnotationBytecodeOffset == currentBytecodeOffset) {
          // Parse the target_type, target_info and target_path fields.
          int currentAnnotationOffset = readTypeAnnotationTarget(
                  context, invisibleTypeAnnotationOffsets[currentInvisibleTypeAnnotationIndex]);
          // Parse the type_index field.
          String annotationDescriptor = readUTF8(currentAnnotationOffset, charBuffer);
          currentAnnotationOffset += 2;
          // Parse num_element_value_pairs and element_value_pairs and visit these values.
          readElementValues(
                  methodVisitor.visitInsnAnnotation(
                          context.currentTypeAnnotationTarget,
                          context.currentTypeAnnotationTargetPath,
                          annotationDescriptor,
                          /* visible = */ false),
                  currentAnnotationOffset,
                  /* named = */ true,
                  charBuffer);
        }
        currentInvisibleTypeAnnotationBytecodeOffset =
                getTypeAnnotationBytecodeOffset(
                        invisibleTypeAnnotationOffsets, ++currentInvisibleTypeAnnotationIndex);
      }
    }
    if (labels[codeLength] != null) {
      methodVisitor.visitLabel(labels[codeLength]);
    }

    // Visit LocalVariableTable and LocalVariableTypeTable attributes.
    if (localVariableTableOffset != 0 && (context.parsingOptions & SKIP_DEBUG) == 0) {
      // The (start_pc, index, signature_index) fields of each entry of the LocalVariableTypeTable.
      int[] typeTable = null;
      if (localVariableTypeTableOffset != 0) {
        typeTable = new int[readUnsignedShort(localVariableTypeTableOffset) * 3];
        currentOffset = localVariableTypeTableOffset + 2;
        int typeTableIndex = typeTable.length;
        while (typeTableIndex > 0) {
          // Store the offset of 'signature_index', and the value of 'index' and 'start_pc'.
          typeTable[--typeTableIndex] = currentOffset + 6;
          typeTable[--typeTableIndex] = readUnsignedShort(currentOffset + 8);
          typeTable[--typeTableIndex] = readUnsignedShort(currentOffset);
          currentOffset += 10;
        }
      }
      int localVariableTableLength = readUnsignedShort(localVariableTableOffset);
      currentOffset = localVariableTableOffset + 2;
      while (localVariableTableLength-- > 0) {
        int startPc = readUnsignedShort(currentOffset);
        int length = readUnsignedShort(currentOffset + 2);
        String name = readUTF8(currentOffset + 4, charBuffer);
        String descriptor = readUTF8(currentOffset + 6, charBuffer);
        int index = readUnsignedShort(currentOffset + 8);
        currentOffset += 10;
        String signature = null;
        if (typeTable != null) {
          for (int i = 0; i < typeTable.length; i += 3) {
            if (typeTable[i] == startPc && typeTable[i + 1] == index) {
              signature = readUTF8(typeTable[i + 2], charBuffer);
              break;
            }
          }
        }
        methodVisitor.visitLocalVariable(
                name, descriptor, signature, labels[startPc], labels[startPc + length], index);
      }
    }

    // Visit the local variable type annotations of the RuntimeVisibleTypeAnnotations attribute.
    if (visibleTypeAnnotationOffsets != null) {
      for (int typeAnnotationOffset : visibleTypeAnnotationOffsets) {
        int targetType = readByte(typeAnnotationOffset);
        if (targetType == TypeReference.LOCAL_VARIABLE
                || targetType == TypeReference.RESOURCE_VARIABLE) {
          // Parse the target_type, target_info and target_path fields.
          currentOffset = readTypeAnnotationTarget(context, typeAnnotationOffset);
          // Parse the type_index field.
          String annotationDescriptor = readUTF8(currentOffset, charBuffer);
          currentOffset += 2;
          // Parse num_element_value_pairs and element_value_pairs and visit these values.
          readElementValues(methodVisitor.visitLocalVariableAnnotation(context.currentTypeAnnotationTarget,
                  context.currentTypeAnnotationTargetPath, context.currentLocalVariableAnnotationRangeStarts,
                  context.currentLocalVariableAnnotationRangeEnds, context.currentLocalVariableAnnotationRangeIndices,
                  annotationDescriptor, /* visible = */ true), currentOffset, /* named = */ true, charBuffer);
        }
      }
    }

    // Visit the local variable type annotations of the RuntimeInvisibleTypeAnnotations attribute.
    if (invisibleTypeAnnotationOffsets != null) {
      for (int typeAnnotationOffset : invisibleTypeAnnotationOffsets) {
        int targetType = readByte(typeAnnotationOffset);
        if (targetType == TypeReference.LOCAL_VARIABLE
                || targetType == TypeReference.RESOURCE_VARIABLE) {
          // Parse the target_type, target_info and target_path fields.
          currentOffset = readTypeAnnotationTarget(context, typeAnnotationOffset);
          // Parse the type_index field.
          String annotationDescriptor = readUTF8(currentOffset, charBuffer);
          currentOffset += 2;
          // Parse num_element_value_pairs and element_value_pairs and visit these values.
          readElementValues(
                  methodVisitor.visitLocalVariableAnnotation(
                          context.currentTypeAnnotationTarget,
                          context.currentTypeAnnotationTargetPath,
                          context.currentLocalVariableAnnotationRangeStarts,
                          context.currentLocalVariableAnnotationRangeEnds,
                          context.currentLocalVariableAnnotationRangeIndices,
                          annotationDescriptor,
                          /* visible = */ false),
                  currentOffset,
                  /* named = */ true,
                  charBuffer);
        }
      }
    }

    // Visit the non standard attributes.
    while (attributes != null) {
      // Copy and reset the nextAttribute field so that it can also be used in MethodWriter.
      Attribute nextAttribute = attributes.nextAttribute;
      attributes.nextAttribute = null;
      methodVisitor.visitAttribute(attributes);
      attributes = nextAttribute;
    }

    // Visit the max stack and max locals values.
    methodVisitor.visitMaxs(maxStack, maxLocals);
  }

  /**
   * Returns the label corresponding to the given bytecode offset. The default implementation of
   * this method creates a label for the given offset if it has not been already created.
   *
   * @param bytecodeOffset a bytecode offset in a method.
   * @param labels the already created labels, indexed by their offset. If a label already exists
   * for bytecodeOffset this method must not create a new one. Otherwise it must store the new
   * label in this array.
   * @return a non null Label, which must be equal to labels[bytecodeOffset].
   */
  protected Label readLabel(final int bytecodeOffset, final Label[] labels) {
    if (labels[bytecodeOffset] == null) {
      labels[bytecodeOffset] = new Label();
    }
    return labels[bytecodeOffset];
  }

  /**
   * Creates a label without the {@link Label#FLAG_DEBUG_ONLY} flag set, for the given bytecode
   * offset. The label is created with a call to {@link #readLabel} and its {@link
   * Label#FLAG_DEBUG_ONLY} flag is cleared.
   *
   * @param bytecodeOffset a bytecode offset in a method.
   * @param labels the already created labels, indexed by their offset.
   * @return a Label without the {@link Label#FLAG_DEBUG_ONLY} flag set.
   */
  private Label createLabel(final int bytecodeOffset, final Label[] labels) {
    Label label = readLabel(bytecodeOffset, labels);
    label.flags &= ~Label.FLAG_DEBUG_ONLY;
    return label;
  }

  /**
   * Creates a label with the {@link Label#FLAG_DEBUG_ONLY} flag set, if there is no already
   * existing label for the given bytecode offset (otherwise does nothing). The label is created
   * with a call to {@link #readLabel}.
   *
   * @param bytecodeOffset a bytecode offset in a method.
   * @param labels the already created labels, indexed by their offset.
   */
  private void createDebugLabel(final int bytecodeOffset, final Label[] labels) {
    if (labels[bytecodeOffset] == null) {
      readLabel(bytecodeOffset, labels).flags |= Label.FLAG_DEBUG_ONLY;
    }
  }

  // ----------------------------------------------------------------------------------------------
  // Methods to parse annotations, type annotations and parameter annotations
  // ----------------------------------------------------------------------------------------------

  /**
   * Parses a Runtime[In]VisibleTypeAnnotations attribute to find the offset of each type_annotation
   * entry it contains, to find the corresponding labels, and to visit the try catch block
   * annotations.
   *
   * @param methodVisitor the method visitor to be used to visit the try catch block annotations.
   * @param context information about the class being parsed.
   * @param runtimeTypeAnnotationsOffset the start offset of a Runtime[In]VisibleTypeAnnotations
   * attribute, excluding the attribute_info's attribute_name_index and attribute_length fields.
   * @param visible true if the attribute to parse is a RuntimeVisibleTypeAnnotations attribute,
   * false it is a RuntimeInvisibleTypeAnnotations attribute.
   * @return the start offset of each entry of the Runtime[In]VisibleTypeAnnotations_attribute's
   * 'annotations' array field.
   */
  private int[] readTypeAnnotations(final MethodVisitor methodVisitor,
          final Context context, final int runtimeTypeAnnotationsOffset, final boolean visible) {
    char[] charBuffer = context.charBuffer;
    byte[] classFileBuffer = this.classFileBuffer;

    int currentOffset = runtimeTypeAnnotationsOffset;
    // Read the num_annotations field and create an array to store the type_annotation offsets.
    int[] typeAnnotationsOffsets = new int[readUnsignedShort(currentOffset)];
    currentOffset += 2;
    // Parse the 'annotations' array field.
    for (int i = 0; i < typeAnnotationsOffsets.length; ++i) {
      typeAnnotationsOffsets[i] = currentOffset;
      // Parse the type_annotation's target_type and the target_info fields. The size of the
      // target_info field depends on the value of target_type.
      int targetType = readInt(currentOffset);
      switch (targetType >>> 24) {
        case TypeReference.LOCAL_VARIABLE, TypeReference.RESOURCE_VARIABLE -> {
          // A localvar_target has a variable size, which depends on the value of their table_length
          // field. It also references bytecode offsets, for which we need labels.
          int tableLength = readUnsignedShort(currentOffset + 1);
          currentOffset += 3;
          while (tableLength-- > 0) {
            int startPc = readUnsignedShort(currentOffset);
            int length = readUnsignedShort(currentOffset + 2);
            // Skip the index field (2 bytes).
            currentOffset += 6;
            createLabel(startPc, context.currentMethodLabels);
            createLabel(startPc + length, context.currentMethodLabels);
          }
        }
        case TypeReference.CAST, TypeReference.CONSTRUCTOR_INVOCATION_TYPE_ARGUMENT, TypeReference.METHOD_INVOCATION_TYPE_ARGUMENT,
             TypeReference.CONSTRUCTOR_REFERENCE_TYPE_ARGUMENT, TypeReference.METHOD_REFERENCE_TYPE_ARGUMENT -> currentOffset += 4;

        case TypeReference.CLASS_EXTENDS, TypeReference.CLASS_TYPE_PARAMETER_BOUND, TypeReference.METHOD_TYPE_PARAMETER_BOUND,
             TypeReference.THROWS, TypeReference.EXCEPTION_PARAMETER, TypeReference.INSTANCEOF, TypeReference.NEW,
             TypeReference.CONSTRUCTOR_REFERENCE, TypeReference.METHOD_REFERENCE -> currentOffset += 3;
        default -> throw new IllegalArgumentException(); // TypeReference type which can't be used in Code attribute, or which is unknown.
      }
      // Parse the rest of the type_annotation structure, starting with the target_path structure
      // (whose size depends on its path_length field).
      int pathLength = readByte(currentOffset);
      if ((targetType >>> 24) == TypeReference.EXCEPTION_PARAMETER) {
        // Parse the target_path structure and create a corresponding TypePath.
        TypePath path = pathLength == 0 ? null : new TypePath(classFileBuffer, currentOffset);
        currentOffset += 1 + 2 * pathLength;
        // Parse the type_index field.
        String annotationDescriptor = readUTF8(currentOffset, charBuffer);
        currentOffset += 2;
        // Parse num_element_value_pairs and element_value_pairs and visit these values.
        currentOffset = readElementValues(methodVisitor.visitTryCatchAnnotation(targetType & 0xFFFFFF00, path, annotationDescriptor, visible),
                currentOffset, /* named = */ true, charBuffer);
      }
      else {
        // We don't want to visit the other target_type annotations, so we just skip them (which
        // requires some parsing because the element_value_pairs array has a variable size). First,
        // skip the target_path structure:
        currentOffset += 3 + 2 * pathLength;
        // Then skip the num_element_value_pairs and element_value_pairs fields (by reading them
        // with a null AnnotationVisitor).
        currentOffset =
                readElementValues(
                        /* annotationVisitor = */ null, currentOffset, /* named = */ true, charBuffer);
      }
    }
    return typeAnnotationsOffsets;
  }

  /**
   * Returns the bytecode offset corresponding to the specified JVMS 'type_annotation' structure, or
   * -1 if there is no such type_annotation of if it does not have a bytecode offset.
   *
   * @param typeAnnotationOffsets the offset of each 'type_annotation' entry in a
   * Runtime[In]VisibleTypeAnnotations attribute, or {@literal null}.
   * @param typeAnnotationIndex the index a 'type_annotation' entry in typeAnnotationOffsets.
   * @return bytecode offset corresponding to the specified JVMS 'type_annotation' structure, or -1
   * if there is no such type_annotation of if it does not have a bytecode offset.
   */
  private int getTypeAnnotationBytecodeOffset(final int[] typeAnnotationOffsets, final int typeAnnotationIndex) {
    if (typeAnnotationOffsets == null
            || typeAnnotationIndex >= typeAnnotationOffsets.length
            || readByte(typeAnnotationOffsets[typeAnnotationIndex]) < TypeReference.INSTANCEOF) {
      return -1;
    }
    return readUnsignedShort(typeAnnotationOffsets[typeAnnotationIndex] + 1);
  }

  /**
   * Parses the header of a JVMS type_annotation structure to extract its target_type, target_info
   * and target_path (the result is stored in the given context), and returns the start offset of
   * the rest of the type_annotation structure.
   *
   * @param context information about the class being parsed. This is where the extracted
   * target_type and target_path must be stored.
   * @param typeAnnotationOffset the start offset of a type_annotation structure.
   * @return the start offset of the rest of the type_annotation structure.
   */
  private int readTypeAnnotationTarget(final Context context, final int typeAnnotationOffset) {
    int currentOffset = typeAnnotationOffset;
    // Parse and store the target_type structure.
    int targetType = readInt(typeAnnotationOffset);
    switch (targetType >>> 24) {
      case TypeReference.CLASS_TYPE_PARAMETER:
      case TypeReference.METHOD_TYPE_PARAMETER:
      case TypeReference.METHOD_FORMAL_PARAMETER:
        targetType &= 0xFFFF0000;
        currentOffset += 2;
        break;
      case TypeReference.FIELD:
      case TypeReference.METHOD_RETURN:
      case TypeReference.METHOD_RECEIVER:
        targetType &= 0xFF000000;
        currentOffset += 1;
        break;
      case TypeReference.LOCAL_VARIABLE:
      case TypeReference.RESOURCE_VARIABLE:
        targetType &= 0xFF000000;
        int tableLength = readUnsignedShort(currentOffset + 1);
        currentOffset += 3;
        context.currentLocalVariableAnnotationRangeStarts = new Label[tableLength];
        context.currentLocalVariableAnnotationRangeEnds = new Label[tableLength];
        context.currentLocalVariableAnnotationRangeIndices = new int[tableLength];
        for (int i = 0; i < tableLength; ++i) {
          int startPc = readUnsignedShort(currentOffset);
          int length = readUnsignedShort(currentOffset + 2);
          int index = readUnsignedShort(currentOffset + 4);
          currentOffset += 6;
          context.currentLocalVariableAnnotationRangeStarts[i] =
                  createLabel(startPc, context.currentMethodLabels);
          context.currentLocalVariableAnnotationRangeEnds[i] =
                  createLabel(startPc + length, context.currentMethodLabels);
          context.currentLocalVariableAnnotationRangeIndices[i] = index;
        }
        break;
      case TypeReference.CAST:
      case TypeReference.CONSTRUCTOR_INVOCATION_TYPE_ARGUMENT:
      case TypeReference.METHOD_INVOCATION_TYPE_ARGUMENT:
      case TypeReference.CONSTRUCTOR_REFERENCE_TYPE_ARGUMENT:
      case TypeReference.METHOD_REFERENCE_TYPE_ARGUMENT:
        targetType &= 0xFF0000FF;
        currentOffset += 4;
        break;
      case TypeReference.CLASS_EXTENDS:
      case TypeReference.CLASS_TYPE_PARAMETER_BOUND:
      case TypeReference.METHOD_TYPE_PARAMETER_BOUND:
      case TypeReference.THROWS:
      case TypeReference.EXCEPTION_PARAMETER:
        targetType &= 0xFFFFFF00;
        currentOffset += 3;
        break;
      case TypeReference.INSTANCEOF:
      case TypeReference.NEW:
      case TypeReference.CONSTRUCTOR_REFERENCE:
      case TypeReference.METHOD_REFERENCE:
        targetType &= 0xFF000000;
        currentOffset += 3;
        break;
      default:
        throw new IllegalArgumentException();
    }
    context.currentTypeAnnotationTarget = targetType;
    // Parse and store the target_path structure.
    int pathLength = readByte(currentOffset);
    context.currentTypeAnnotationTargetPath =
            pathLength == 0 ? null : new TypePath(classFileBuffer, currentOffset);
    // Return the start offset of the rest of the type_annotation structure.
    return currentOffset + 1 + 2 * pathLength;
  }

  /**
   * Reads a Runtime[In]VisibleParameterAnnotations attribute and makes the given visitor visit it.
   *
   * @param methodVisitor the visitor that must visit the parameter annotations.
   * @param context information about the class being parsed.
   * @param runtimeParameterAnnotationsOffset the start offset of a
   * Runtime[In]VisibleParameterAnnotations attribute, excluding the attribute_info's
   * attribute_name_index and attribute_length fields.
   * @param visible true if the attribute to parse is a RuntimeVisibleParameterAnnotations
   * attribute, false it is a RuntimeInvisibleParameterAnnotations attribute.
   */
  private void readParameterAnnotations(final MethodVisitor methodVisitor,
          final Context context, final int runtimeParameterAnnotationsOffset, final boolean visible) {
    int currentOffset = runtimeParameterAnnotationsOffset;
    int numParameters = classFileBuffer[currentOffset++] & 0xFF;
    methodVisitor.visitAnnotableParameterCount(numParameters, visible);
    char[] charBuffer = context.charBuffer;
    for (int i = 0; i < numParameters; ++i) {
      int numAnnotations = readUnsignedShort(currentOffset);
      currentOffset += 2;
      while (numAnnotations-- > 0) {
        // Parse the type_index field.
        String annotationDescriptor = readUTF8(currentOffset, charBuffer);
        currentOffset += 2;
        // Parse num_element_value_pairs and element_value_pairs and visit these values.
        currentOffset =
                readElementValues(
                        methodVisitor.visitParameterAnnotation(i, annotationDescriptor, visible),
                        currentOffset,
                        /* named = */ true,
                        charBuffer);
      }
    }
  }

  /**
   * Reads the element values of a JVMS 'annotation' structure and makes the given visitor visit
   * them. This method can also be used to read the values of the JVMS 'array_value' field of an
   * annotation's 'element_value'.
   *
   * @param annotationVisitor the visitor that must visit the values.
   * @param annotationOffset the start offset of an 'annotation' structure (excluding its type_index
   * field) or of an 'array_value' structure.
   * @param named if the annotation values are named or not. This should be true to parse the values
   * of a JVMS 'annotation' structure, and false to parse the JVMS 'array_value' of an
   * annotation's element_value.
   * @param charBuffer the buffer used to read strings in the constant pool.
   * @return the end offset of the JVMS 'annotation' or 'array_value' structure.
   */
  private int readElementValues(final AnnotationVisitor annotationVisitor,
          final int annotationOffset, final boolean named, final char[] charBuffer) {
    int currentOffset = annotationOffset;
    // Read the num_element_value_pairs field (or num_values field for an array_value).
    int numElementValuePairs = readUnsignedShort(currentOffset);
    currentOffset += 2;
    if (named) {
      // Parse the element_value_pairs array.
      while (numElementValuePairs-- > 0) {
        String elementName = readUTF8(currentOffset, charBuffer);
        currentOffset =
                readElementValue(annotationVisitor, currentOffset + 2, elementName, charBuffer);
      }
    }
    else {
      // Parse the array_value array.
      while (numElementValuePairs-- > 0) {
        currentOffset =
                readElementValue(annotationVisitor, currentOffset, /* elementName= */ null, charBuffer);
      }
    }
    if (annotationVisitor != null) {
      annotationVisitor.visitEnd();
    }
    return currentOffset;
  }

  /**
   * Reads a JVMS 'element_value' structure and makes the given visitor visit it.
   *
   * @param annotationVisitor the visitor that must visit the element_value structure.
   * @param elementValueOffset the start offset in {@link #classFileBuffer} of the element_value
   * structure to be read.
   * @param elementName the name of the element_value structure to be read, or {@literal null}.
   * @param charBuffer the buffer used to read strings in the constant pool.
   * @return the end offset of the JVMS 'element_value' structure.
   */
  private int readElementValue(final AnnotationVisitor annotationVisitor,
          final int elementValueOffset, final String elementName, final char[] charBuffer) {
    int currentOffset = elementValueOffset;
    byte[] classFileBuffer = this.classFileBuffer;
    if (annotationVisitor == null) {
      return switch (classFileBuffer[currentOffset] & 0xFF) {
        case 'e' -> // enum_const_value
                currentOffset + 5;
        case '@' -> // annotation_value
                readElementValues(null, currentOffset + 3, /* named = */ true, charBuffer);
        case '[' -> // array_value
                readElementValues(null, currentOffset + 1, /* named = */ false, charBuffer);
        default -> currentOffset + 3;
      };
    }
    int[] cpInfoOffsets = this.cpInfoOffsets;
    switch (classFileBuffer[currentOffset++] & 0xFF) {
      case 'B' -> { // const_value_index, CONSTANT_Integer
        annotationVisitor.visit(
                elementName, (byte) readInt(cpInfoOffsets[readUnsignedShort(currentOffset)]));
        currentOffset += 2;
      }
      case 'C' -> { // const_value_index, CONSTANT_Integer
        annotationVisitor.visit(
                elementName, (char) readInt(cpInfoOffsets[readUnsignedShort(currentOffset)]));
        currentOffset += 2;
      } // const_value_index, CONSTANT_Double
      // const_value_index, CONSTANT_Float
      // const_value_index, CONSTANT_Integer
      case 'D', 'F', 'I', 'J' -> { // const_value_index, CONSTANT_Long
        annotationVisitor.visit(
                elementName, readConst(readUnsignedShort(currentOffset), charBuffer));
        currentOffset += 2;
      }
      case 'S' -> { // const_value_index, CONSTANT_Integer
        annotationVisitor.visit(
                elementName, (short) readInt(cpInfoOffsets[readUnsignedShort(currentOffset)]));
        currentOffset += 2;
      }
      case 'Z' -> { // const_value_index, CONSTANT_Integer
        annotationVisitor.visit(elementName,
                readInt(cpInfoOffsets[readUnsignedShort(currentOffset)]) == 0 ? Boolean.FALSE : Boolean.TRUE);
        currentOffset += 2;
      }
      case 's' -> { // const_value_index, CONSTANT_Utf8
        annotationVisitor.visit(elementName, readUTF8(currentOffset, charBuffer));
        currentOffset += 2;
      }
      case 'e' -> { // enum_const_value
        annotationVisitor.visitEnum(
                elementName,
                readUTF8(currentOffset, charBuffer),
                readUTF8(currentOffset + 2, charBuffer));
        currentOffset += 4;
      }
      case 'c' -> { // class_info
        annotationVisitor.visit(elementName, Type.forDescriptor(readUTF8(currentOffset, charBuffer)));
        currentOffset += 2;
      }
      case '@' -> {// annotation_value
        currentOffset = readElementValues(
                annotationVisitor.visitAnnotation(elementName, readUTF8(currentOffset, charBuffer)),
                currentOffset + 2, true, charBuffer);
      }
      case '[' -> { // array_value
        int numValues = readUnsignedShort(currentOffset);
        currentOffset += 2;
        if (numValues == 0) {
          return readElementValues(annotationVisitor.visitArray(elementName),
                  currentOffset - 2, /* named = */ false, charBuffer);
        }
        switch (classFileBuffer[currentOffset] & 0xFF) {
          case 'B' -> {
            byte[] byteValues = new byte[numValues];
            for (int i = 0; i < numValues; i++) {
              byteValues[i] = (byte) readInt(cpInfoOffsets[readUnsignedShort(currentOffset + 1)]);
              currentOffset += 3;
            }
            annotationVisitor.visit(elementName, byteValues);
          }
          case 'Z' -> {
            boolean[] booleanValues = new boolean[numValues];
            for (int i = 0; i < numValues; i++) {
              booleanValues[i] = readInt(cpInfoOffsets[readUnsignedShort(currentOffset + 1)]) != 0;
              currentOffset += 3;
            }
            annotationVisitor.visit(elementName, booleanValues);
          }
          case 'S' -> {
            short[] shortValues = new short[numValues];
            for (int i = 0; i < numValues; i++) {
              shortValues[i] = (short) readInt(cpInfoOffsets[readUnsignedShort(currentOffset + 1)]);
              currentOffset += 3;
            }
            annotationVisitor.visit(elementName, shortValues);
          }
          case 'C' -> {
            char[] charValues = new char[numValues];
            for (int i = 0; i < numValues; i++) {
              charValues[i] = (char) readInt(cpInfoOffsets[readUnsignedShort(currentOffset + 1)]);
              currentOffset += 3;
            }
            annotationVisitor.visit(elementName, charValues);
          }
          case 'I' -> {
            int[] intValues = new int[numValues];
            for (int i = 0; i < numValues; i++) {
              intValues[i] = readInt(cpInfoOffsets[readUnsignedShort(currentOffset + 1)]);
              currentOffset += 3;
            }
            annotationVisitor.visit(elementName, intValues);
          }
          case 'J' -> {
            long[] longValues = new long[numValues];
            for (int i = 0; i < numValues; i++) {
              longValues[i] = readLong(cpInfoOffsets[readUnsignedShort(currentOffset + 1)]);
              currentOffset += 3;
            }
            annotationVisitor.visit(elementName, longValues);
          }
          case 'F' -> {
            float[] floatValues = new float[numValues];
            for (int i = 0; i < numValues; i++) {
              floatValues[i] = Float.intBitsToFloat(
                      readInt(cpInfoOffsets[readUnsignedShort(currentOffset + 1)]));
              currentOffset += 3;
            }
            annotationVisitor.visit(elementName, floatValues);
          }
          case 'D' -> {
            double[] doubleValues = new double[numValues];
            for (int i = 0; i < numValues; i++) {
              doubleValues[i] = Double.longBitsToDouble(
                      readLong(cpInfoOffsets[readUnsignedShort(currentOffset + 1)]));
              currentOffset += 3;
            }
            annotationVisitor.visit(elementName, doubleValues);
          }
          default -> {
            currentOffset = readElementValues(
                    annotationVisitor.visitArray(elementName),
                    currentOffset - 2,
                    /* named = */ false,
                    charBuffer);
          }
        }
      }
      default -> throw new IllegalArgumentException();
    }
    return currentOffset;
  }

  // ----------------------------------------------------------------------------------------------
  // Methods to parse stack map frames
  // ----------------------------------------------------------------------------------------------

  /**
   * Computes the implicit frame of the method currently being parsed (as defined in the given
   * {@link Context}) and stores it in the given context.
   *
   * @param context information about the class being parsed.
   */
  private void computeImplicitFrame(final Context context) {
    String methodDescriptor = context.currentMethodDescriptor;
    Object[] locals = context.currentFrameLocalTypes;
    int numLocal = 0;
    if ((context.currentMethodAccessFlags & Opcodes.ACC_STATIC) == 0) {
      if ("<init>".equals(context.currentMethodName)) {
        locals[numLocal++] = Opcodes.UNINITIALIZED_THIS;
      }
      else {
        locals[numLocal++] = readClass(header + 2, context.charBuffer);
      }
    }
    // Parse the method descriptor, one argument type descriptor at each iteration. Start by
    // skipping the first method descriptor character, which is always '('.
    int currentMethodDescritorOffset = 1;
    while (true) {
      int currentArgumentDescriptorStartOffset = currentMethodDescritorOffset;
      switch (methodDescriptor.charAt(currentMethodDescritorOffset++)) {
        case 'F' -> locals[numLocal++] = Opcodes.FLOAT;
        case 'J' -> locals[numLocal++] = Opcodes.LONG;
        case 'D' -> locals[numLocal++] = Opcodes.DOUBLE;
        case 'Z', 'C', 'B', 'S', 'I' -> locals[numLocal++] = Opcodes.INTEGER;
        case '[' -> {
          while (methodDescriptor.charAt(currentMethodDescritorOffset) == '[') {
            ++currentMethodDescritorOffset;
          }
          if (methodDescriptor.charAt(currentMethodDescritorOffset) == 'L') {
            ++currentMethodDescritorOffset;
            while (methodDescriptor.charAt(currentMethodDescritorOffset) != ';') {
              ++currentMethodDescritorOffset;
            }
          }
          locals[numLocal++] =
                  methodDescriptor.substring(
                          currentArgumentDescriptorStartOffset, ++currentMethodDescritorOffset);
        }
        case 'L' -> {
          while (methodDescriptor.charAt(currentMethodDescritorOffset) != ';') {
            ++currentMethodDescritorOffset;
          }
          locals[numLocal++] =
                  methodDescriptor.substring(
                          currentArgumentDescriptorStartOffset + 1, currentMethodDescritorOffset++);
        }
        default -> {
          context.currentFrameLocalCount = numLocal;
          return;
        }
      }
    }
  }

  /**
   * Reads a JVMS 'stack_map_frame' structure and stores the result in the given {@link Context}
   * object. This method can also be used to read a full_frame structure, excluding its frame_type
   * field (this is used to parse the legacy StackMap attributes).
   *
   * @param stackMapFrameOffset the start offset in {@link #classFileBuffer} of the
   * stack_map_frame_value structure to be read, or the start offset of a full_frame structure
   * (excluding its frame_type field).
   * @param compressed true to read a 'stack_map_frame' structure, false to read a 'full_frame'
   * structure without its frame_type field.
   * @param expand if the stack map frame must be expanded. See {@link #EXPAND_FRAMES}.
   * @param context where the parsed stack map frame must be stored.
   * @return the end offset of the JVMS 'stack_map_frame' or 'full_frame' structure.
   */
  private int readStackMapFrame(final int stackMapFrameOffset,
          final boolean compressed, final boolean expand, final Context context) {
    int currentOffset = stackMapFrameOffset;
    final char[] charBuffer = context.charBuffer;
    final Label[] labels = context.currentMethodLabels;
    int frameType;
    if (compressed) {
      // Read the frame_type field.
      frameType = classFileBuffer[currentOffset++] & 0xFF;
    }
    else {
      frameType = Frame.FULL_FRAME;
      context.currentFrameOffset = -1;
    }
    int offsetDelta;
    context.currentFrameLocalCountDelta = 0;
    if (frameType < Frame.SAME_LOCALS_1_STACK_ITEM_FRAME) {
      offsetDelta = frameType;
      context.currentFrameType = Opcodes.F_SAME;
      context.currentFrameStackCount = 0;
    }
    else if (frameType < Frame.RESERVED) {
      offsetDelta = frameType - Frame.SAME_LOCALS_1_STACK_ITEM_FRAME;
      currentOffset =
              readVerificationTypeInfo(
                      currentOffset, context.currentFrameStackTypes, 0, charBuffer, labels);
      context.currentFrameType = Opcodes.F_SAME1;
      context.currentFrameStackCount = 1;
    }
    else if (frameType >= Frame.SAME_LOCALS_1_STACK_ITEM_FRAME_EXTENDED) {
      offsetDelta = readUnsignedShort(currentOffset);
      currentOffset += 2;
      if (frameType == Frame.SAME_LOCALS_1_STACK_ITEM_FRAME_EXTENDED) {
        currentOffset =
                readVerificationTypeInfo(
                        currentOffset, context.currentFrameStackTypes, 0, charBuffer, labels);
        context.currentFrameType = Opcodes.F_SAME1;
        context.currentFrameStackCount = 1;
      }
      else if (frameType >= Frame.CHOP_FRAME && frameType < Frame.SAME_FRAME_EXTENDED) {
        context.currentFrameType = Opcodes.F_CHOP;
        context.currentFrameLocalCountDelta = Frame.SAME_FRAME_EXTENDED - frameType;
        context.currentFrameLocalCount -= context.currentFrameLocalCountDelta;
        context.currentFrameStackCount = 0;
      }
      else if (frameType == Frame.SAME_FRAME_EXTENDED) {
        context.currentFrameType = Opcodes.F_SAME;
        context.currentFrameStackCount = 0;
      }
      else if (frameType < Frame.FULL_FRAME) {
        int local = expand ? context.currentFrameLocalCount : 0;
        for (int k = frameType - Frame.SAME_FRAME_EXTENDED; k > 0; k--) {
          currentOffset =
                  readVerificationTypeInfo(
                          currentOffset, context.currentFrameLocalTypes, local++, charBuffer, labels);
        }
        context.currentFrameType = Opcodes.F_APPEND;
        context.currentFrameLocalCountDelta = frameType - Frame.SAME_FRAME_EXTENDED;
        context.currentFrameLocalCount += context.currentFrameLocalCountDelta;
        context.currentFrameStackCount = 0;
      }
      else {
        final int numberOfLocals = readUnsignedShort(currentOffset);
        currentOffset += 2;
        context.currentFrameType = Opcodes.F_FULL;
        context.currentFrameLocalCountDelta = numberOfLocals;
        context.currentFrameLocalCount = numberOfLocals;
        for (int local = 0; local < numberOfLocals; ++local) {
          currentOffset =
                  readVerificationTypeInfo(
                          currentOffset, context.currentFrameLocalTypes, local, charBuffer, labels);
        }
        final int numberOfStackItems = readUnsignedShort(currentOffset);
        currentOffset += 2;
        context.currentFrameStackCount = numberOfStackItems;
        for (int stack = 0; stack < numberOfStackItems; ++stack) {
          currentOffset =
                  readVerificationTypeInfo(
                          currentOffset, context.currentFrameStackTypes, stack, charBuffer, labels);
        }
      }
    }
    else {
      throw new IllegalArgumentException();
    }
    context.currentFrameOffset += offsetDelta + 1;
    createLabel(context.currentFrameOffset, labels);
    return currentOffset;
  }

  /**
   * Reads a JVMS 'verification_type_info' structure and stores it at the given index in the given
   * array.
   *
   * @param verificationTypeInfoOffset the start offset of the 'verification_type_info' structure to
   * read.
   * @param frame the array where the parsed type must be stored.
   * @param index the index in 'frame' where the parsed type must be stored.
   * @param charBuffer the buffer used to read strings in the constant pool.
   * @param labels the labels of the method currently being parsed, indexed by their offset. If the
   * parsed type is an ITEM_Uninitialized, a new label for the corresponding NEW instruction is
   * stored in this array if it does not already exist.
   * @return the end offset of the JVMS 'verification_type_info' structure.
   */
  private int readVerificationTypeInfo(final int verificationTypeInfoOffset,
          final Object[] frame, final int index, final char[] charBuffer, final Label[] labels) {
    int currentOffset = verificationTypeInfoOffset;
    int tag = classFileBuffer[currentOffset++] & 0xFF;
    switch (tag) {
      case Frame.ITEM_TOP -> frame[index] = Opcodes.TOP;
      case Frame.ITEM_LONG -> frame[index] = Opcodes.LONG;
      case Frame.ITEM_NULL -> frame[index] = Opcodes.NULL;
      case Frame.ITEM_FLOAT -> frame[index] = Opcodes.FLOAT;
      case Frame.ITEM_DOUBLE -> frame[index] = Opcodes.DOUBLE;
      case Frame.ITEM_INTEGER -> frame[index] = Opcodes.INTEGER;
      case Frame.ITEM_UNINITIALIZED_THIS -> frame[index] = Opcodes.UNINITIALIZED_THIS;
      case Frame.ITEM_OBJECT -> {
        frame[index] = readClass(currentOffset, charBuffer);
        currentOffset += 2;
      }
      case Frame.ITEM_UNINITIALIZED -> {
        frame[index] = createLabel(readUnsignedShort(currentOffset), labels);
        currentOffset += 2;
      }
      default -> throw new IllegalArgumentException();
    }
    return currentOffset;
  }

  // ----------------------------------------------------------------------------------------------
  // Methods to parse attributes
  // ----------------------------------------------------------------------------------------------

  /**
   * Returns the offset in {@link #classFileBuffer} of the first ClassFile's 'attributes' array
   * field entry.
   *
   * @return the offset in {@link #classFileBuffer} of the first ClassFile's 'attributes' array
   * field entry.
   */
  final int getFirstAttributeOffset() {
    // Skip the access_flags, this_class, super_class, and interfaces_count fields (using 2 bytes
    // each), as well as the interfaces array field (2 bytes per interface).
    int currentOffset = header + 8 + readUnsignedShort(header + 6) * 2;

    // Read the fields_count field.
    int fieldsCount = readUnsignedShort(currentOffset);
    currentOffset += 2;
    // Skip the 'fields' array field.
    while (fieldsCount-- > 0) {
      // Invariant: currentOffset is the offset of a field_info structure.
      // Skip the access_flags, name_index and descriptor_index fields (2 bytes each), and read the
      // attributes_count field.
      int attributesCount = readUnsignedShort(currentOffset + 6);
      currentOffset += 8;
      // Skip the 'attributes' array field.
      while (attributesCount-- > 0) {
        // Invariant: currentOffset is the offset of an attribute_info structure.
        // Read the attribute_length field (2 bytes after the start of the attribute_info) and skip
        // this many bytes, plus 6 for the attribute_name_index and attribute_length fields
        // (yielding the total size of the attribute_info structure).
        currentOffset += 6 + readInt(currentOffset + 2);
      }
    }

    // Skip the methods_count and 'methods' fields, using the same method as above.
    int methodsCount = readUnsignedShort(currentOffset);
    currentOffset += 2;
    while (methodsCount-- > 0) {
      int attributesCount = readUnsignedShort(currentOffset + 6);
      currentOffset += 8;
      while (attributesCount-- > 0) {
        currentOffset += 6 + readInt(currentOffset + 2);
      }
    }

    // Skip the ClassFile's attributes_count field.
    return currentOffset + 2;
  }

  /**
   * Reads the BootstrapMethods attribute to compute the offset of each bootstrap method.
   *
   * @param maxStringLength a conservative estimate of the maximum length of the strings contained
   * in the constant pool of the class.
   * @return the offsets of the bootstrap methods.
   */
  private int[] readBootstrapMethodsAttribute(final int maxStringLength) {
    char[] charBuffer = new char[maxStringLength];
    int currentAttributeOffset = getFirstAttributeOffset();
    for (int i = readUnsignedShort(currentAttributeOffset - 2); i > 0; --i) {
      // Read the attribute_info's attribute_name and attribute_length fields.
      String attributeName = readUTF8(currentAttributeOffset, charBuffer);
      int attributeLength = readInt(currentAttributeOffset + 2);
      currentAttributeOffset += 6;
      if (Constants.BOOTSTRAP_METHODS.equals(attributeName)) {
        // Read the num_bootstrap_methods field and create an array of this size.
        int[] result = new int[readUnsignedShort(currentAttributeOffset)];
        // Compute and store the offset of each 'bootstrap_methods' array field entry.
        int currentBootstrapMethodOffset = currentAttributeOffset + 2;
        for (int j = 0; j < result.length; ++j) {
          result[j] = currentBootstrapMethodOffset;
          // Skip the bootstrap_method_ref and num_bootstrap_arguments fields (2 bytes each),
          // as well as the bootstrap_arguments array field (of size num_bootstrap_arguments * 2).
          currentBootstrapMethodOffset +=
                  4 + readUnsignedShort(currentBootstrapMethodOffset + 2) * 2;
        }
        return result;
      }
      currentAttributeOffset += attributeLength;
    }
    throw new IllegalArgumentException();
  }

  /**
   * Reads a non standard JVMS 'attribute' structure in {@link #classFileBuffer}.
   *
   * @param attributePrototypes prototypes of the attributes that must be parsed during the visit of
   * the class. Any attribute whose type is not equal to the type of one the prototypes will not
   * be parsed: its byte array value will be passed unchanged to the ClassWriter.
   * @param type the type of the attribute.
   * @param offset the start offset of the JVMS 'attribute' structure in {@link #classFileBuffer}.
   * The 6 attribute header bytes (attribute_name_index and attribute_length) are not taken into
   * account here.
   * @param length the length of the attribute's content (excluding the 6 attribute header bytes).
   * @param charBuffer the buffer to be used to read strings in the constant pool.
   * @param codeAttributeOffset the start offset of the enclosing Code attribute in {@link
   * #classFileBuffer}, or -1 if the attribute to be read is not a code attribute. The 6
   * attribute header bytes (attribute_name_index and attribute_length) are not taken into
   * account here.
   * @param labels the labels of the method's code, or {@literal null} if the attribute to be read
   * is not a code attribute.
   * @return the attribute that has been read.
   */
  private Attribute readAttribute(final Attribute[] attributePrototypes, final String type,
          final int offset, final int length, final char[] charBuffer, final int codeAttributeOffset, final Label[] labels) {
    for (Attribute attributePrototype : attributePrototypes) {
      if (attributePrototype.type.equals(type)) {
        return attributePrototype.read(
                this, offset, length, charBuffer, codeAttributeOffset, labels);
      }
    }
    return new Attribute(type).read(this, offset, length, null, -1, null);
  }

  // -----------------------------------------------------------------------------------------------
  // Utility methods: low level parsing
  // -----------------------------------------------------------------------------------------------

  /**
   * Returns the number of entries in the class's constant pool table.
   *
   * @return the number of entries in the class's constant pool table.
   */
  public int getItemCount() {
    return cpInfoOffsets.length;
  }

  /**
   * Returns the start offset in this {@link ClassReader} of a JVMS 'cp_info' structure (i.e. a
   * constant pool entry), plus one. <i>This method is intended for {@link Attribute} sub classes,
   * and is normally not needed by class generators or adapters.</i>
   *
   * @param constantPoolEntryIndex the index a constant pool entry in the class's constant pool
   * table.
   * @return the start offset in this {@link ClassReader} of the corresponding JVMS 'cp_info'
   * structure, plus one.
   */
  public int getItem(final int constantPoolEntryIndex) {
    return cpInfoOffsets[constantPoolEntryIndex];
  }

  /**
   * Returns a conservative estimate of the maximum length of the strings contained in the class's
   * constant pool table.
   *
   * @return a conservative estimate of the maximum length of the strings contained in the class's
   * constant pool table.
   */
  public int getMaxStringLength() {
    return maxStringLength;
  }

  /**
   * Reads a byte value in this {@link ClassReader}. <i>This method is intended for {@link
   * Attribute} sub classes, and is normally not needed by class generators or adapters.</i>
   *
   * @param offset the start offset of the value to be read in this {@link ClassReader}.
   * @return the read value.
   */
  public int readByte(final int offset) {
    return classFileBuffer[offset] & 0xFF;
  }

  /**
   * Reads several bytes in this {@link ClassReader}. <i>This method is intended for {@link
   * Attribute} sub classes, and is normally not needed by class generators or adapters.</i>
   *
   * @param offset the start offset of the bytes to be read in this {@link ClassReader}.
   * @param length the number of bytes to read.
   * @return the read bytes.
   */
  public byte[] readBytes(final int offset, final int length) {
    byte[] result = new byte[length];
    System.arraycopy(classFileBuffer, offset, result, 0, length);
    return result;
  }

  /**
   * Reads an unsigned short value in this {@link ClassReader}. <i>This method is intended for
   * {@link Attribute} sub classes, and is normally not needed by class generators or adapters.</i>
   *
   * @param offset the start index of the value to be read in this {@link ClassReader}.
   * @return the read value.
   */
  public int readUnsignedShort(final int offset) {
    byte[] classBuffer = classFileBuffer;
    return ((classBuffer[offset] & 0xFF) << 8) | (classBuffer[offset + 1] & 0xFF);
  }

  /**
   * Reads a signed short value in this {@link ClassReader}. <i>This method is intended for {@link
   * Attribute} sub classes, and is normally not needed by class generators or adapters.</i>
   *
   * @param offset the start offset of the value to be read in this {@link ClassReader}.
   * @return the read value.
   */
  public short readShort(final int offset) {
    byte[] classBuffer = classFileBuffer;
    return (short) (((classBuffer[offset] & 0xFF) << 8) | (classBuffer[offset + 1] & 0xFF));
  }

  /**
   * Reads a signed int value in this {@link ClassReader}. <i>This method is intended for {@link
   * Attribute} sub classes, and is normally not needed by class generators or adapters.</i>
   *
   * @param offset the start offset of the value to be read in this {@link ClassReader}.
   * @return the read value.
   */
  public int readInt(final int offset) {
    byte[] classBuffer = classFileBuffer;
    return ((classBuffer[offset] & 0xFF) << 24)
            | ((classBuffer[offset + 1] & 0xFF) << 16)
            | ((classBuffer[offset + 2] & 0xFF) << 8)
            | (classBuffer[offset + 3] & 0xFF);
  }

  /**
   * Reads a signed long value in this {@link ClassReader}. <i>This method is intended for {@link
   * Attribute} sub classes, and is normally not needed by class generators or adapters.</i>
   *
   * @param offset the start offset of the value to be read in this {@link ClassReader}.
   * @return the read value.
   */
  public long readLong(final int offset) {
    long l1 = readInt(offset);
    long l0 = readInt(offset + 4) & 0xFFFFFFFFL;
    return (l1 << 32) | l0;
  }

  /**
   * Reads a CONSTANT_Utf8 constant pool entry in this {@link ClassReader}. <i>This method is
   * intended for {@link Attribute} sub classes, and is normally not needed by class generators or
   * adapters.</i>
   *
   * @param offset the start offset of an unsigned short value in this {@link ClassReader}, whose
   * value is the index of a CONSTANT_Utf8 entry in the class's constant pool table.
   * @param charBuffer the buffer to be used to read the string. This buffer must be sufficiently
   * large. It is not automatically resized.
   * @return the String corresponding to the specified CONSTANT_Utf8 entry.
   */
  // DontCheck(AbbreviationAsWordInName): can't be renamed (for backward binary compatibility).
  public String readUTF8(final int offset, final char[] charBuffer) {
    int constantPoolEntryIndex = readUnsignedShort(offset);
    if (offset == 0 || constantPoolEntryIndex == 0) {
      return null;
    }
    return readUtf(constantPoolEntryIndex, charBuffer);
  }

  /**
   * Reads a CONSTANT_Utf8 constant pool entry in {@link #classFileBuffer}.
   *
   * @param constantPoolEntryIndex the index of a CONSTANT_Utf8 entry in the class's constant pool
   * table.
   * @param charBuffer the buffer to be used to read the string. This buffer must be sufficiently
   * large. It is not automatically resized.
   * @return the String corresponding to the specified CONSTANT_Utf8 entry.
   */
  final String readUtf(final int constantPoolEntryIndex, final char[] charBuffer) {
    String[] constantUtf8Values = this.constantUtf8Values;
    String value = constantUtf8Values[constantPoolEntryIndex];
    if (value != null) {
      return value;
    }
    int cpInfoOffset = cpInfoOffsets[constantPoolEntryIndex];
    return constantUtf8Values[constantPoolEntryIndex] =
            readUtf(cpInfoOffset + 2, readUnsignedShort(cpInfoOffset), charBuffer);
  }

  /**
   * Reads an UTF8 string in {@link #classFileBuffer}.
   *
   * @param utfOffset the start offset of the UTF8 string to be read.
   * @param utfLength the length of the UTF8 string to be read.
   * @param charBuffer the buffer to be used to read the string. This buffer must be sufficiently
   * large. It is not automatically resized.
   * @return the String corresponding to the specified UTF8 string.
   */
  private String readUtf(final int utfOffset, final int utfLength, final char[] charBuffer) {
    int currentOffset = utfOffset;
    int endOffset = currentOffset + utfLength;
    int strLength = 0;
    byte[] classBuffer = classFileBuffer;
    while (currentOffset < endOffset) {
      int currentByte = classBuffer[currentOffset++];
      if ((currentByte & 0x80) == 0) {
        charBuffer[strLength++] = (char) (currentByte & 0x7F);
      }
      else if ((currentByte & 0xE0) == 0xC0) {
        charBuffer[strLength++] =
                (char) (((currentByte & 0x1F) << 6) + (classBuffer[currentOffset++] & 0x3F));
      }
      else {
        charBuffer[strLength++] = (char) (((currentByte & 0xF) << 12)
                + ((classBuffer[currentOffset++] & 0x3F) << 6)
                + (classBuffer[currentOffset++] & 0x3F));
      }
    }
    return new String(charBuffer, 0, strLength);
  }

  /**
   * Reads a CONSTANT_Class, CONSTANT_String, CONSTANT_MethodType, CONSTANT_Module or
   * CONSTANT_Package constant pool entry in {@link #classFileBuffer}. <i>This method is intended
   * for {@link Attribute} sub classes, and is normally not needed by class generators or
   * adapters.</i>
   *
   * @param offset the start offset of an unsigned short value in {@link #classFileBuffer}, whose
   * value is the index of a CONSTANT_Class, CONSTANT_String, CONSTANT_MethodType,
   * CONSTANT_Module or CONSTANT_Package entry in class's constant pool table.
   * @param charBuffer the buffer to be used to read the item. This buffer must be sufficiently
   * large. It is not automatically resized.
   * @return the String corresponding to the specified constant pool entry.
   */
  private String readStringish(final int offset, final char[] charBuffer) {
    // Get the start offset of the cp_info structure (plus one), and read the CONSTANT_Utf8 entry
    // designated by the first two bytes of this cp_info.
    return readUTF8(cpInfoOffsets[readUnsignedShort(offset)], charBuffer);
  }

  /**
   * Reads a CONSTANT_Class constant pool entry in this {@link ClassReader}. <i>This method is
   * intended for {@link Attribute} sub classes, and is normally not needed by class generators or
   * adapters.</i>
   *
   * @param offset the start offset of an unsigned short value in this {@link ClassReader}, whose
   * value is the index of a CONSTANT_Class entry in class's constant pool table.
   * @param charBuffer the buffer to be used to read the item. This buffer must be sufficiently
   * large. It is not automatically resized.
   * @return the String corresponding to the specified CONSTANT_Class entry.
   */
  public String readClass(final int offset, final char[] charBuffer) {
    return readStringish(offset, charBuffer);
  }

  /**
   * Reads a CONSTANT_Module constant pool entry in this {@link ClassReader}. <i>This method is
   * intended for {@link Attribute} sub classes, and is normally not needed by class generators or
   * adapters.</i>
   *
   * @param offset the start offset of an unsigned short value in this {@link ClassReader}, whose
   * value is the index of a CONSTANT_Module entry in class's constant pool table.
   * @param charBuffer the buffer to be used to read the item. This buffer must be sufficiently
   * large. It is not automatically resized.
   * @return the String corresponding to the specified CONSTANT_Module entry.
   */
  public String readModule(final int offset, final char[] charBuffer) {
    return readStringish(offset, charBuffer);
  }

  /**
   * Reads a CONSTANT_Package constant pool entry in this {@link ClassReader}. <i>This method is
   * intended for {@link Attribute} sub classes, and is normally not needed by class generators or
   * adapters.</i>
   *
   * @param offset the start offset of an unsigned short value in this {@link ClassReader}, whose
   * value is the index of a CONSTANT_Package entry in class's constant pool table.
   * @param charBuffer the buffer to be used to read the item. This buffer must be sufficiently
   * large. It is not automatically resized.
   * @return the String corresponding to the specified CONSTANT_Package entry.
   */
  public String readPackage(final int offset, final char[] charBuffer) {
    return readStringish(offset, charBuffer);
  }

  /**
   * Reads a CONSTANT_Dynamic constant pool entry in {@link #classFileBuffer}.
   *
   * @param constantPoolEntryIndex the index of a CONSTANT_Dynamic entry in the class's constant
   * pool table.
   * @param charBuffer the buffer to be used to read the string. This buffer must be sufficiently
   * large. It is not automatically resized.
   * @return the ConstantDynamic corresponding to the specified CONSTANT_Dynamic entry.
   */
  private ConstantDynamic readConstantDynamic(final int constantPoolEntryIndex, final char[] charBuffer) {
    ConstantDynamic constantDynamic = constantDynamicValues[constantPoolEntryIndex];
    if (constantDynamic != null) {
      return constantDynamic;
    }
    int cpInfoOffset = cpInfoOffsets[constantPoolEntryIndex];
    int nameAndTypeCpInfoOffset = cpInfoOffsets[readUnsignedShort(cpInfoOffset + 2)];
    String name = readUTF8(nameAndTypeCpInfoOffset, charBuffer);
    String descriptor = readUTF8(nameAndTypeCpInfoOffset + 2, charBuffer);
    int bootstrapMethodOffset = bootstrapMethodOffsets[readUnsignedShort(cpInfoOffset)];
    Handle handle = (Handle) readConst(readUnsignedShort(bootstrapMethodOffset), charBuffer);
    Object[] bootstrapMethodArguments = new Object[readUnsignedShort(bootstrapMethodOffset + 2)];
    bootstrapMethodOffset += 4;
    for (int i = 0; i < bootstrapMethodArguments.length; i++) {
      bootstrapMethodArguments[i] = readConst(readUnsignedShort(bootstrapMethodOffset), charBuffer);
      bootstrapMethodOffset += 2;
    }
    return constantDynamicValues[constantPoolEntryIndex] =
            new ConstantDynamic(name, descriptor, handle, bootstrapMethodArguments);
  }

  /**
   * Reads a numeric or string constant pool entry in this {@link ClassReader}. <i>This method is
   * intended for {@link Attribute} sub classes, and is normally not needed by class generators or
   * adapters.</i>
   *
   * @param constantPoolEntryIndex the index of a CONSTANT_Integer, CONSTANT_Float, CONSTANT_Long,
   * CONSTANT_Double, CONSTANT_Class, CONSTANT_String, CONSTANT_MethodType,
   * CONSTANT_MethodHandle or CONSTANT_Dynamic entry in the class's constant pool.
   * @param charBuffer the buffer to be used to read strings. This buffer must be sufficiently
   * large. It is not automatically resized.
   * @return the {@link Integer}, {@link Float}, {@link Long}, {@link Double}, {@link String},
   * {@link Type}, {@link Handle} or {@link ConstantDynamic} corresponding to the specified
   * constant pool entry.
   */
  public Object readConst(final int constantPoolEntryIndex, final char[] charBuffer) {
    int cpInfoOffset = cpInfoOffsets[constantPoolEntryIndex];
    return switch (classFileBuffer[cpInfoOffset - 1]) {
      case Symbol.CONSTANT_INTEGER_TAG -> readInt(cpInfoOffset);
      case Symbol.CONSTANT_FLOAT_TAG -> Float.intBitsToFloat(readInt(cpInfoOffset));
      case Symbol.CONSTANT_LONG_TAG -> readLong(cpInfoOffset);
      case Symbol.CONSTANT_DOUBLE_TAG -> Double.longBitsToDouble(readLong(cpInfoOffset));
      case Symbol.CONSTANT_CLASS_TAG -> Type.forInternalName(readUTF8(cpInfoOffset, charBuffer));
      case Symbol.CONSTANT_STRING_TAG -> readUTF8(cpInfoOffset, charBuffer);
      case Symbol.CONSTANT_METHOD_TYPE_TAG -> Type.forMethod(readUTF8(cpInfoOffset, charBuffer));
      case Symbol.CONSTANT_DYNAMIC_TAG -> readConstantDynamic(constantPoolEntryIndex, charBuffer);
      case Symbol.CONSTANT_METHOD_HANDLE_TAG -> {
        int referenceKind = readByte(cpInfoOffset);
        int referenceCpInfoOffset = cpInfoOffsets[readUnsignedShort(cpInfoOffset + 1)];
        int nameAndTypeCpInfoOffset = cpInfoOffsets[readUnsignedShort(referenceCpInfoOffset + 2)];
        String owner = readClass(referenceCpInfoOffset, charBuffer);
        String name = readUTF8(nameAndTypeCpInfoOffset, charBuffer);
        String descriptor = readUTF8(nameAndTypeCpInfoOffset + 2, charBuffer);
        boolean isInterface = classFileBuffer[referenceCpInfoOffset - 1] == Symbol.CONSTANT_INTERFACE_METHODREF_TAG;
        yield new Handle(referenceKind, owner, name, descriptor, isInterface);
      }
      default -> throw new IllegalArgumentException();
    };
  }
}
